Choosing an API framework for your TypeScript backend

Choosing an API framework for your TypeScript backend

If you’re a backend developer, you’re probably aware of Node.js. If you’re not, it’s a run-time environment that includes everything required to execute a program written in JavaScript. It’s both efficient and scalable, which has made it one of the best ways to develop server-side and network applications.

Node’s popularity in backend development has been accelerated by the plethora of JavaScript-based frameworks available that make building out an API easier (such as Express). However, developing large applications can be difficult using plain JavaScript. Since it is not a a strongly-typed language, it’s harder to keep track of the many complex data types flowing around your application. This often leads to more runtime errors, because you won’t get as many compile-time errors as you type.

Due to these limitations, many developers have turned to TypeScript. TypeScript is the strongly-typed version of JavaScript designed for large applications. Several frameworks for the language have emerged as a result, but which one should you choose?

In this blog post, we will present a comparison of two TypeScript-based Node.js frameworks that we researched for our engineering team: NestJS and tsoa.

In the rest of the post, we’ll go through the following sections, discuss benefits and drawbacks, and then present our final thoughts.

  • Background
  • Core Components
  • Validation and Transformation
  • OpenAPI
  • Documentation
  • Performance
  • Final Thoughts

Background

Both Nest and tsoa are frameworks for server-side applications written in TypeScript. Nest does give you the option to use plain JavaScript, but you’ll have more difficulty taking full advantage of the framework.

Both frameworks are built on top of Express by default, but are platform-agnostic. Nest allows you to switch out for fastify out-of-the-box and tsoa currently supports Hapi and Koa. They are also frontend-agnostic, but it’s worth noting that Nest was heavily inspired by Angular, so it does integrate particularly well with it.

Nest and tsoa both have the ability to autogenerate OpenAPI specs based on your TypeScript model definitions. At the time of writing, no other frameworks offer this capability to our knowledge.

At a glance, the frameworks are very similar. However, there are several important aspects where they differ that may make one better suited to your needs.

Core Components

Nest and tsoa follow an OOP (Object Oriented Programming) style. They were built this way to fully leverage TypeScript decorators and their metadata, which is only available for classes and is otherwise lost when using interfaces.

The core component classes that make up Nest and tsoa’s structure are the controller, provider (aka service), module (only in Nest), and DTO.

Controller

For each route, there will be a controller class whose purpose is to handle the HTTP request and “route” the request to a provider (service). The controller is the component where you will configure features like headers, custom responses, parameter validation, and many more via decorators. Decorators associate classes with the required metadata for Nest to create a routing map.

Provider / Service

The controller handles the HTTP side of things and delegates the core logic of a request to another class, called a provider in Nest (tsoa just calls it a service class). It’s where you would write code to interact with a database, for example. This component is the same in tsoa and Nest, the only difference being Nest requires the @Injectable decorator at the top of the class. This is because Nest is built around a strong design pattern called Dependency injection.

Module

Nest has the notion of a module while tsoa does not. The module is a way of encapsulating controllers and providers and organizing them so Nest can organize the application structure. Every module supports a set of closely-related behaviors so that it is highly reusable. Part of the reason Nest uses modules it that they make it easier to follow the microservices architecture style of development, which Nest natively supports. Modules do not exist in tsoa, so recreating their organizational advantages requires a third-party library or custom implementation.

DTO

DTOs are the class definitions which make your code more robust and type-safe. You’ll have these in both frameworks, but Nest is slightly more prescriptive about how they are named and organized (more on this in the OpenAPI section). DTOs are meant to be specific to certain CRUD operations and separate from interfaces. They allow you to define validation logic and documentation in the same code block.

Why should I care?

The core components create a structure useful not only for development, but also for maintenance.

The structure isn’t unique to NestJS or tsoa; however, when combined with built-in decorators and a focus on dependency injection, Nest better enforces where sections of code go and what they are responsible for. This reduces decision-making during development, as well as time spent navigating existing code.

Validation and Transformation

Preventing bad data types from getting through (validation) and ensuring correct data types get sent through (transformation) are key to API development. They are also two of the main reasons developers choose to use TypeScript. Nest and tsoa differ quite significantly in how they implement these features.

Nest

Nest encapsulates validation and transformation with its own system of classes called Pipes. Pipes operate on the arguments that a controller route receives. The validation and/or transformation that a Pipe performs occurs before the route’s method is called, and returns a descriptive error message if invoked.

An example of a transformation pipe is ParseIntPipe , which will attempt to convert the argument to an integer. There are others like ParseFloatPipe and ParseBoolPipe. Nest provides a total of 8 out-of-the-box which are very convenient. You can also make custom pipes by implementing their PipeTransform interface, which is very well-documented here.

For validation, Nest offers an out-of-the-box ValidationPipe based on the powerful class-validator package. The purpose of the pipe is to either return the value a controller receives unchanged or throw an error.

Validation is configured by applying decorators within your DTO classes. For example, if you have a class for a CreateUser method and a User has an email field, you could apply the @IsEmail decorator above the field. Nest is good about integrating with the class-validator package, which significantly speeds up development and improves code readability. The full list of decorators can be found here.

Another key benefit of the Pipes system is that it forces you to have a single point of responsibility for your validation logic—the type definition itself (the DTO).

tsoa

Tsoa is more bare-bones when it comes to validation. It doesn’t prescribe its own syntax with a system like Pipes. Tsoa will throw runtime validation errors based on your types by default, though, whereas you do have to set up a global ValidationPipe for that in Nest. Using tsoa means you don't have to learn about Pipes, and your app is automatically equipped with basic validation from the start.

However, applying more granular and complex validation logic is made easier with the class-validator and class-transformer packages. Their integration with Nest was one of our favorite features, so we were disappointed to find that tsoa has no documentation on how it integrates with them. Instead, they provide their own suite of decorators in a page here. You’ll find that it is harder to follow.

Why should I care?

The main difference here is Nest's Pipes system, which leverages decorators from the class-validator and class-transformer package. We found it lead to a cleaner development experience and made our DTOs more readable. However, as Pipes is unique to Nest, it is a new standard to familiarize yourself with, which may not be what you want if you're making a simple API.

Additionally, we found that having simple, global validation was easy in tsoa because it came by default. This can suit many small projects or spin-ups. However, tsoa doesn't have documentation on how the framework integrates with the class-validator and class-transformer decorators.

We would recommend tsoa for smaller teams who are not expecting to need scalable, robust, and granular validation. If you anticipate those to be important, like for large teams and enterprises, Nest is a better option.

OpenAPI

You might be wondering why we’re comparing these 2 frameworks in the first place. The reason is because they are the only two that natively support autogeneration of OpenAPI specs. At the time of writing, no other TypeScript-based frameworks support this.

Nest

In Nest, you have to annotate DTO fields with @ApiProperty() decorator for it to show up in the OpenAPI spec. You also have to include the required property depending on the property question mark. Boilerplate like this can build up, so to eliminate it Nest provides an easy-to-configure CLI plugin. The plugin will add the appropriate decorators on the fly, so you don't have to have @ApiProperty() decorators scattered around. For instance, using the plugin can reduce a DTO from this:

export class CreateUserDto {
  @ApiProperty()
  password: string;

  @ApiProperty({ enum: RoleEnum, default: [], isArray: true })
  roles: RoleEnum[] = [];

  @ApiProperty({ required: false, default: true })
  isEnabled?: boolean = true;
}

To just this:

export class CreateUserDto {
  password: string;
  roles: RoleEnum[] = [];
  isEnabled?: boolean = true;
}

Note: Your filenames must have a .dto.ts or .entity.ts suffix for the plugin to pick it up, but this can be changed in Nest’s configuration.

The CLI also has a comments introspection feature, which means it can provide descriptions and example values for properties based on JSdoc comments. And if you ever want full control over a definition, you can always override the plugin via @ApiProperty() .

OpenAPI Example

tsoa

Tsoa is slightly more pre-configured for openAPI because a large part of its philosophy is based around it.

Without labelling anything in your DTOs and without configuring a plugin, tsoa will generate OpenAPI docs based on TypeScript interfaces. It doesn’t need to be a class with a .dto suffix like in Nest (though it can be for organizational purposes).

Tsoa also comes pre-configured to use JSDoc comments for examples and descriptions. The process is essentially the same as in Nest, with descriptions provided above endpoints, parameters, properties, etc. You’re able to provide examples by using an @Example decorator that show up in the docs.

OpenAPI- Example Value

Why should I care?

OpenAPI is tsoa’s bread and butter and notably a large portion of its documentation is related to it, so it’s easy to find. If openAPI is one of the only top concerns of yours, and your project is relatively simple, tsoa is probably a better choice.

Out-of-the-box Nest requires a bit more boilerplate and configuration. Much of that is eliminated with the plugin, though, which we believe inherently enforces better organization with a naming convention (.dto files). The pattern integrates well with their Pipes system.

OpenAPI is not Nest's main selling point, but we feel it is just as good with some fairly simple configuration, so we recommend it for general-purpose, larger projects and tsoa for spin-ups.

Documentation

Documentation is one of the most differentiating aspects between Nest and tsoa. Nest has a massive amount of documentation compared to tsoa. It has official enterprise support, online courses, and even an online merch store.

Nest

We found Nest’s documentation was clearer and easier to follow along. When following tsoa’s ‘Getting Started’ guide, for example, we encountered code blocks where we had to rely on our own debugging skills to resolve. Nest’s quick start guide was totally smooth on the other hand.

In addition to clarity, it’s worth mentioning that there’s more to document for Nest because it simply has more capabilities. It has an entire section dedicated to its native support for microservices, for example, with sections for streaming platforms such as Kafka. There’s also entire sections on WebSockets and GraphQL. Nest supports a plethora of capabilities that can all integrate with each other; it gives the framework more of an “ecosystem” feel.

tsoa

Having a larger set of capabilities doesn’t necessarily make Nest the right choice for every project. If you have a strong sense of what your framework will need to have, tsoa’s documentation may not be a hinderance. For example, if you want a framework that is lightweight, built for TypeScript, handles simple validation out-of-the-box, is openAPI-compliant, and aren’t expecting to need many other features, tsoa is great. Plus since there’s less documentation to sift through, it’s easier to find.

However, if you're new to API development or aren't sure what you may need, it's worth noting that tsoa is significantly less popular than Nest. Nest has over 48k stars on GitHub compared to tsoa’s 2.2k. Despite far less adoption, tsoa has 89 open GitHub issues, many of which are unacknowledged. In comparison, Nest has 39 open issues, and nearly all of them contain heavy discussion.

Why should I care?

If you want access to a fully-fledged set of features that are well-documented, Nest is the better option. We think it’s also a good framework to use even if you are starting small, because it forces you to develop in an extensible way. It scales well thanks to great documentation of its own as well as heavy adoption.

Tsoa’s documentation is relatively lackluster, which can be a significant issue when looking for examples and demos, especially if you aren’t a seasoned backend programmer. Nevertheless, it can be negligible if you have a relatively simple set of requirements that you don’t expect to grow a lot.

Overall, the depth and breadth of Nest’s documentation give it a major plus over tsoa for most use cases.

Performance

We all want fast apps, so performance is something to consider when deciding between API frameworks. Recall, both Nest and tsoa are abstractions on top of Express by default, so how do their relative overheads actually compare?

To answer this, we set up todo APIs using each framework which included 6 CRUD endpoints. Both APIs were connected to a local Postgres DB.

To run our performance test, we used the open-source load testing tool k6. The tool reports many metrics, one of which is the  http_req_duration, a measure of the average amount of time a request takes to complete. Another metric is the number of iterations, or the total number of times the virtual users (VUs) executed the script. VUs is a number specifying the number of concurrent sessions hitting the API. There are plenty more metrics you can check out here, but for our purposes these 2 serve as a good proxy of relative speed.

Each test followed a ramp-up and ramp-down scenario like this:

Test Profile

And the results were as follows:

From these results, there’s minimal difference in performance as VUs and iterations grow. Thus, we don't believe there's a meaningful difference in terms of performance for most use cases.

Our tests aimed to simulate a general range of use cases, but if you'd like to run a different test (such as stress testing or hammering the endpoint), feel free to clone our repos at the links below and try them for yourself.

GitHub - alteryx/NestJS-Todo-API: Simple todo API using NestJS framework
Simple todo API using NestJS framework. Contribute to alteryx/NestJS-Todo-API development by creating an account on GitHub.
Nest API
GitHub - alteryx/tsoa-Todo-API: Simple todo API using tsoa framework
Simple todo API using tsoa framework. Contribute to alteryx/tsoa-Todo-API development by creating an account on GitHub.
tsoa API

Final Thoughts

Both these frameworks have a similar profile, but our conclusion is that Nest is the smarter choice for most use cases.

Nest provides a more complete and broad ecosystem than tsoa. The framework has a lot more features out-of-the-box, better support, and offers many more integrations. It’s also more prescriptive and opinionated, having its own language for certain concepts such as Pipes. We feel there’s sufficient room for customization, though, and subscribing to their system makes the development experience cleaner by properly structuring your app.

For less complex projects with requirements that are not expected to grow a lot, Nest’s heavier learning curve may not be worth it, especially if you are coming from a pure Express background. However, tsoa's adoption is far lower, which makes it difficult to recommend in cases where readability, standardization, and maintenance are essential to managing your codebase.


Acknowledgements

Special thanks to Chris Cornell who helped in the research and development to make this article possible.