OpenAPI Code-First mit NestJS
Photo by Robert Thiemann on Unsplash
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: