Node.js with TSOA and Request-Body-Validation

In this article, I'll show you how to use TSOA to validate your REST interface and address its limitations. With this knowledge, you can develop a stable and reliable API. Let's get started!

3 minutes

This article assumes you have already set up TSOA. You can find the details here. The example is intentionally simple to illustrate the different mechanisms: creating a new user via our interface.

Requirements

We will handle the creation of a new entity via a POST request. First and last name should be required fields, and the salutation or email address should be optional in the request body. The backend determines the entity ID; however, we expect the newly assigned ID, along with the remaining data for the new entity, to be returned as JSON. The email address should be entered correctly (from syntax perspective).

TSOA Implementation

To implement the requirements mentioned above, we receive support from TSOA:

  • The route is defined as POST /users using the decorators @Route() and @Post().
  • The mandatory data types of the request body are linked to the type UserCreateBody using @Body in the method signature.
  • The response is defined as a response type in the method signature. In our case, it's a Promise because we're writing asynchronous code: Promise<UserCreateResponse>.
  • We define a required field as a type with an @minLength requirement. Given the two-letter names, I would be cautious about the restrictions here. A minimum length of 3 would already cause problems with “Xu”. The text after min-length definition is the error message if we receive invalid data.
  • For optional fields, we forgo the minimum length and allow a null value in the request body. I prefer this variant to handling an empty string as unset. Reading the type immediately clarifies my intention because we define string | null.
type UserCreateBody = {
  /**
   * The user's degree
   */
  degree: string | null
  /**
   * First name of the user
   * @minLength 2 Please enter a first name
   */
  firstName: string
  /**
   * Last name of the user
   * @minLength 2 Please enter a last name
   */
  lastName: string
  /**  
   * The user's email
   */
  email: string | null
}

type UserCreateResponse = UserCreateBody & {
  id: string
}

@Route('users')
export class UserController extends Controller {
  @Post()  
  public async create(@Body() requestBody: UserCreateBody): Promise<UserCreateResponse> {
    Logger.debug(`[UserController] Create entity`, requestBody)  

    // TSOA does not support a simple way of validating the email format  
    // Therefore, we use our own validation here
    const email = requestBody.email
    if (email && !isEMail(email)) {  
      throw new Error('Email validation failed') // we should be more smart here
    }  

    // We do not want the callee to decide the user ID.
    // This depends on your requirements, which approach makes sense.
    const newUserId = v4()  

    // ... e.g., call Repository to create user

    this.setStatus(201)  

    return {  
      id: newUserId,  
      degree: requestBody.degree,  
      firstName: requestBody.firstName,  
      lastName: requestBody.lastName,  
      email: requestBody.email,  
    }
  }  
}

Special Case: Email Address

When validating email addresses, you have the option to extend TSOA or specify a regular expression for the syntax of the expected string.

type Example = {
  /**
   * @pattern ^\S+@\S+\.\S+$ // <- a too simple definition
   */
  email: string
}

I deliberately chose the “email” example. Regular expressions for this are extensive and complex. Therefore, it makes sense to use libraries such as zod or to implement (and test) a proven method in a central location. The latter approach avoids the need for an external library and the associated burden of maintenance and security updates.

Based on my reasoning, I prefer to reuse a proven method. For this purpose, I defined the isEmail() function in a Node.js-based project and included it where needed.

Conclusion

This small example demonstrates how TSOA can be used to implement the interface requirements in a readable way. The weaknesses I've mentioned stem from the need for more rigorous validation. TSOA doesn't claim to meet this requirement; it aims to comply with the underlying Open API definition. Therefore, it's our responsibility as developers to implement this. The appropriate place for this is the controller, before the validated data is passed on.

Interested readers might have some questions: What does the response look like in case of an error? What response body and status code are returned? And how does throw new Error('Email validation failed') fit into the picture?

The answers to these questions will follow in the next posts. The goal is to comply with RFC 7807 and translate errors accordingly.

call to action background image

Subscribe to my newsletter

Receive once a month news from the areas of software development and communication peppered with book and link recommendations.