abshoff.dev: Web, Mobile & Cloud!

OpenAPI Code-First mit NestJS

Vogelnest

Als Fortsetzung meines vorangegangenen Artikels API-First vs. Code-First zeige ich Dir in diesem Artikel, wie Du mit NestJS und TypeScript-Annotationen OpenAPI-Definitionen erzeugen kannst. Es handelt sich somit um einen weiteren Code-First-Ansatz, der ein weiter verbreitetes Node.js-Framework nutzt.

Nest (oder NestJS) bezeichnet sich selbst als progressives Node.js-Framework, mit dem man effiziente, betriebssichere und skalierbare Server-Anwendungen schreiben kann. Es unterstützt TypeScript und macht sich viele bewährte Muster u. a. aus der Angular-Welt zunutze. Frontend-Entwickler werden sich schnell zurechtfinden.

Als Erstes erstellen wir über das Nest CLI ein neues Projekt. Die Dokumentation empfiehlt die Installation des Nest CLI globale über npm i -g @nestjs/cli, ich bevorzuge jedoch die lokale Ausführung über den Node.js-Package-Runner npx als Alias: alias nest="npx @nestjs/cli".

nest new nestjs-openapi-code-first --package-manager npm
cd nestjs-openapi-code-first

Danach können wir mit npm start einen Express-basierten Entwicklungsserver starten. Die Verzeichnisstruktur sieht wie folgt aus:

nestjs-openapi-code-first/
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

Um OpenAPI generieren zu können, installieren wir noch das Swagger-Modul:

npm i --save @nestjs/swagger

Als nächstes kann Du eine Entität src/customer.entity.ts erstellen. Mit der Annotation @ApiProperty kann Du die Eigenschaften für die OpenAPI-Definition näher beschreiben und z. B. Beispielausprägungen angeben.

import { ApiProperty } from "@nestjs/swagger";

export enum CustomerClassification {
  A = "A",
  B = "B",
  C = "C",
}

export class CustomerEntity {
  @ApiProperty({
    description: "Key of the data record",
    example: "1",
  })
  id: string;

  @ApiProperty({
    description: "ABC classification of the customer",
    example: "A",
    enum: CustomerClassification,
  })
  classification: CustomerClassification;

  constructor(partial: Partial<CustomerEntity>) {
    Object.assign(this, partial);
  }
}

Danach passen wir den vorhandenen Service unter src/app.service.ts wie folgt an:

import { Injectable } from "@nestjs/common";
import { CustomerEntity, CustomerClassification } from "./customer.entity";

@Injectable()
export class AppService {
  getCustomer(id: string): CustomerEntity {
    return new CustomerEntity({
      id: `${id}`,
      classification: CustomerClassification.A,
    });
  }
}

Und schließlich passen wir den vorhandenen Controller unter src/app.controller.ts an. Mit der @Get Annotation beschreibst Du den Pfad Deines Endpunkts und mit @ApiOkResponse definierst Du den Typ für die OpenAPI-Definition.

import { Controller, Get, Param } from '@nestjs/common';
import { AppService } from './app.service';
import { CustomerEntity } from './customer.entity';
import { ApiOkResponse } from '@nestjs/swagger';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('customer/:id')
  @ApiOkResponse({
    type: CustomerEntity,
  })
  getCustomer(@Param('id') id: string): CustomerEntity {
    return this.appService.getCustomer(id);
  }
}

Wenn Du den lokalen Server mit npm start startest, kannst Du mit dem HTTP-Werkzeug curl eine Anfrage stellen:

curl http://localhost:3000/customer/4711

Die Antwort sieht so aus:

{"id":"4711","classification":"A"}

OpenAPI-Definition erstellen

Wenn Du eine OpenAPI-Datei erstellen möchtest, hilft folgendes Skript, das Du unter src/main.openapi.ts ablegen kann und das die openapi.yaml im darüberliegenden Verzeichnis erstellt bzw. überschreibt.

import * as fs from "fs";
import * as YAML from "yaml";
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";

async function writeOpenAPIToFile() {
  const app = await NestFactory.create(AppModule);
  const options = new DocumentBuilder()
    .setTitle("nestjs-openapi-code-first API")
    .setDescription("Example API")
    .setVersion("1.0")
    .build();
  const document = SwaggerModule.createDocument(app, options);
  const yaml = YAML.stringify(document);
  fs.writeFileSync("./openapi.yaml", yaml);
}
writeOpenAPIToFile();

Mit dem Befehl npm run build && node dist/main.openapi.js führst Du es aus. Du kannst es auch als Skript in der package.json hinterlegen und mit npm run openapi ausführen.

{
...
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "openapi": "npm run build && node dist/main.openapi.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
...
}

Das Ergebnis sieht so aus:

penapi: 3.0.0
paths:
  /customer:
    get:
      operationId: AppController_getCustomer
      parameters: []
      responses:
        "200":
          description: ""
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerEntity"
info:
  title: nestjs-openapi-code-first API
  description: Example API
  version: "1.0"
  contact: {}
tags: []
servers: []
components:
  schemas:
    CustomerEntity:
      type: object
      properties:
        id:
          type: string
          description: Key of the data record
          example: "1"
        classification:
          type: string
          description: ABC classification of the customer
          example: A
          enum:
            - A
            - B
            - C
      required:
        - id
        - classification

Ressource anlegen

Mit dem folgenden Befehl kannst Du eine neue CRUD Resource anlegen, also eine Entität, die Du als REST-API mit den Operationen Create, Read, Update und Delete manipulieren kannst. Als Beispiel möchte ich eine Entität für Aufträge mit dem Namen order erstellen.

nest generate resource order

Das CLI fragt, ob die Vorlage als REST-API und mit den CRUD-Einstiegspunkten genutzt werden soll, was Du bejahen kannst. Das zuvor generierte App-Module wird automatisch angepasst und die generierten Data-Transfer-Objects (dto) dienen der Definitionen der Eingabe bei Create und Update.

? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/order/order.controller.spec.ts (566 bytes)
CREATE src/order/order.controller.ts (904 bytes)
CREATE src/order/order.module.ts (247 bytes)
CREATE src/order/order.service.spec.ts (453 bytes)
CREATE src/order/order.service.ts (621 bytes)
CREATE src/order/dto/create-order.dto.ts (31 bytes)
CREATE src/order/dto/update-order.dto.ts (168 bytes)
CREATE src/order/entities/order.entity.ts (22 bytes)
UPDATE src/app.module.ts (312 bytes)

Azure Functions App erstellen

Falls Du die Anwendung als Azure Function App ausführen möchtest, kannst Du mit Hilfe einer Nest-Bibliothek die benötigten Einstiegspunkte generieren:

nest add @nestjs/azure-func-http

Diese Variante wird mit folgendem Befehl gestartet:

npm run start:azure

Fazit

Auch mit Nest ist es möglich mit dem Code-First-Ansatz zu programmieren. Darüber hinaus implementiert das Framework viele Entwurfsmuster, die Dir die Entwicklung deutlich erleichtern können.

Tags: