Node.js mit TSOA und Request-Body-Validierung

In diesem Artikel zeige ich, wie TSOA zur Überprüfung der REST-Schnittstelle verwendet wird und wie ich mit den Einschränkungen umgehe. Mit diesem Wissen kannst du eine stabile und verlässliche API entwickeln. Packen wir es an!

3 Minuten

Dieser Artikel setzt voraus, dass du TSOA bereits eingerichtet hast. Die Details dazu findest du hier. Das Beispiel ist bewusst einfach gehalten, um die unterschiedlichen Mechanismen zu verdeutlichen: das Anlegen eines neuen Nutzers über unsere Schnittstelle.

Anforderung

Wir kümmern uns um die Anlage einer neuen Entität per POST-Aufruf. Hierbei sollen Vor- und Nachname als Pflichtfelder und die Ansprache oder die E-Mail-Adresse als optionale Werte im Request-Body angegeben werden. Die ID der Entität bestimmt das Backend; wir erwarten jedoch die neu vergebene ID als Antwort, zusammen mit den restlichen Daten der neuen Entität, als JSON. Bei der Angabe der E-Mail-Adresse ist die syntaktische Korrektheit zu prüfen.

TSOA-Umsetzung

Um die oben genannten Anforderungen umzusetzen, erhalten wir Unterstützung von TSOA:

  • Die Route wird über die Decorator @Route() und @Post() als POST /users definiert
  • Die verbindlichen Datentypen des Request-Body werden mittels @Body in der Methodensignatur mit dem Typ UserCreateBody verknüpft.
  • Die Response-Definition erfolgt über die Methodensignatur als Response-Typ. In unserem Fall als Promise, weil wir asynchronen Code schreiben: Promise<UserCreateResponse>.
  • Ein Pflichtfeld definieren wir als Typ mit einer @minLength-Anforderung. Da es Namen mit zwei Buchstaben gibt, würde ich hier vorsichtig mit den Restriktionen umgehen. Eine Mindestlänge von 3 würde bei "Xu" bereits Probleme verursachen. Der Text nach der MinLength-Definition ist die Fehlermeldung.
  • Bei optionalen Feldern verzichten wir auf die Mindestlänge und erlauben einen null-Wert im Request-Body. Diese Variante bevorzuge ich dem Leerstring als nicht gesetzt. Das Lesen des Typs verdeutlicht sofort meine Absicht.
type UserCreateBody = {
  /**
   * Die Ansprache des Mitarbeiters
   */
  degree: string | null
  /**
   * Vorname des Mitarbeiters
   * @minLength 2 Bitte einen Vornamen angeben
   */
  firstName: string
  /**
   * Nachname des Mitarbeiters
   * @minLength 2 Bitte einen Nachnamen angeben
   */
  lastName: string
  /**  
   * Die E-Mail-Adresse des Mitarbeiters
   */
  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,  
    }
  }  
}

Sonderfall E-Mail-Adresse

Bei der E-Mail-Adressenprüfung besteht die Möglichkeit, TSOA zu erweitern oder einen RegEx für die Syntax des erwarteten Strings anzugeben.

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

Ich habe bewusst das Beispiel "E-Mail" gewählt. Der reguläre Ausdruck ist umfangreich und komplex. Da bietet es sich an, Bibliotheken wie z. B. zod zu nutzen oder selbst an zentraler Stelle eine bewährte Methode zu implementieren (und zu testen). Letzterer Ansatz ermöglicht den Verzicht auf eine fremde Bibliothek sowie die damit verbundene Last für Wartung und Security-Updates.

Entsprechend meiner eigenen Argumentation bevorzuge ich die Variante, eine bewährte Methode wiederzuverwenden. Dafür habe ich mir die Funktion isEmail() in einem Node.js-Base-Projekt definiert und binde sie dort ein, wo ich sie benötige.

Fazit

Das kleine Beispiel zeigt, wie mithilfe von TSOA die Anforderungen an die Schnittstelle gut lesbar umgesetzt werden. Die angedeuteten Schwächen sehe ich in einer komplexeren Validierung. TSOA hat nicht den Anspruch, dies zu erfüllen, sondern will der zugrunde liegenden Open-API-Definition entsprechen. Entsprechend ist es unsere Aufgabe als Entwickelnde, dies einzubauen. Der richtige Ort ist der Controller, bevor er die validierten Daten weitergibt.

Die geneigten Lesenden stellen sich vielleicht einige wichtige Fragen: Wie sieht der Response im Falle eines Fehlers aus? Welchen Response-Body und welchen Statuscode werden zurückgegeben? Und wie passt throw new Error('Email validation failed') ins Bild?

Die Beantwortung dieser Fragen erfolgt in den nächsten Beiträgen. Ziel ist es, dem Standard RFC 7807 zu entsprechen und Fehler korrekt zu übersetzen.

call to action background image

Abonniere meinen Newsletter

Erhalte einmal im Monat Nachrichten aus den Bereichen Softwareentwicklung und Kommunikation gespikt mit Buch- und Linkempfehlungen.