4. OpenAPI
4. OpenAPI
Een API moet duidelijk opgebouwd zijn en vlot gebruikt kunnen worden. Hiervoor is het handig als er documentatie voorzien is. Deze les bespreken we hoe we, via JSDoc strings, documentatie kunnen voorzien voor een api.
De startbestanden voor deze les zijn grotendeels gebaseerd op het lesvoorbeeld van les 2. Op vlak van functionaliteit zijn er geen verschillen, de startbestanden zijn wel uitgebreid met enkele docs-string.
Startbestanden
OpenAPI Specification
De OpenAPI Specification (OAS) is een open standaard die gebruikt kan worden om een API te beschrijven op een taal-agnostische manier. Dit betekent dat de standaard gelezen kan worden door iedereen en niet afhangt van de programmeertaal. De specificatie bevat enkel informatie over de HTTP-methodes, endpoints, parameters, request- en response bodies, en statuscodes. Informatie die dus niets te maken heeft met de implementatiedetails. Een OAS kan geïmporteerd worden in Postman, zo moeten de requests niet manueel aangemaakt worden als je met een nieuwe, via OpenAPI gedocumenteerde, API wilt gebruiken.
Een OpenAPI specificatie kan op bijzonder veel manieren gegenereerd worden, zo is er bijvoorbeeld de grafische tool Swagger Editor. Een specificatie die via deze tool opgebouwd is kan vervolgens gebruikt worden om via Swagger Codegen server stubs te genereren, dit is een skelet waar de HTTP endpoints parameters, routes, ... inzitten, maar waarin alle implementatiedetails ontbreken.
Verder kan de specificatie gegenereerd worden via TypeScript annotations door middel van de tsoa bibliotheek (deze werkt enkel als je code een bepaalde structuur heeft) of via @nestjs/swagger (werkt enkel als je Nest.js, een Node.js framework gebruikt).
Via Swagger UI kan de OpenAPI specificatie, ongeacht de manier waarop deze gebouwd is, gevisualiseerd worden in een eenvoudig te gebruiken webinterface.
NPM Libraries
Tijdens deze les hebben we twee libraries nodig. Om de specificatie te genereren op basis van de commentaar-strings bij elk endpoint, gebruiken we swagger-jsdoc. Om de specificatie vervolgens te visualiseren wordt swagger-ui-express. Merk op dat de TypeScript bindings ook geïnstalleerd moeten worden.
OpenAPI configureren
We beginnen met een nieuwe route aan te maken waarop we de documentatie tonen. Binnen de configuratie van de API-specificatie, hierin gebruiken we het versienummer uit package.json, op deze manier wordt de API-spec automatisch geüpdatet als een nieuwe versie van de API gereleased wordt.
Nadat de specificatie gemaakt is, kunnen we deze doorgeven aan swagger-ui zodat de UI getoond kan worden op basis van deze specificatie.
Merk op dat we twee verschillende routes definiëren, via de eerste route wordt de UI getoond en via de tweede kan de UI gedownload worden als JSON formaat (om te importeren in Postman).
import swaggerJSDoc, {OAS3Options} from 'swagger-jsdoc'
import {version} from '../../../package.json'
import express, {Request, Response} from 'express'
import swaggerUi from 'swagger-ui-express'
const docs = express.Router()
const options: OAS3Options = {
// Whether to throw when parsing errors. Defaults to false.
failOnErrors: true,
definition: {
// Use V3 of the OpenAPI Specification.
openapi: '3.0.0',
// The URLs that can be used to visit the API.
servers: [
{url: 'http://localhost:3000'},
],
info: {
// The title for the docs page.
title: 'TM Campus API docs',
// The APP version, taken from package.json.
version,
},
},
// The location where the API routes and schemas
// are located.
apis: ['./src/routes/**/*.ts'],
}
const swaggerSpec = swaggerJSDoc(options)
docs.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
docs.get('/docs.json', (_: Request, res: Response) => {
res.send(swaggerSpec)
})
export default docsMerk op dat bovenstaande code een foutmelding toont over de JSON import.
Zoals de foutmelding zegt, moeten we de resolveJsonModule vlag toevoegen aan de TypeScript compiler options.
{
"compilerOptions": {
"target": "es2017",
"lib": [
"es2022"
],
"module": "ES2022",
"moduleResolution": "nodenext",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"skipLibCheck": true,
"resolveJsonModule": true,
},
"include": [
"./src/**/*.ts"
]
}Nu de docs gedefinieerd zijn, kunnen we de / route aanpassen zodat deze automatisch re-direct naar de /docs route.
import express, {Request, Response} from 'express'
import campuses from './campuses/campuses.js'
import programs from './programs/programs.js'
import docs from './docs/docs.js'
const index = express.Router()
index.get('/', (_: Request, res: Response) => {
res.redirect('/docs')
})
index.use(docs)
index.use(campuses)
index.use(programs)
export default indexAls we nu navigeren naar http://localhost:3000, krijgen we het onderstaande te zien.

Documentatie schrijven
Documentatie wordt gedefinieerd via een doc-string waarin de @openapi of @swagger annotatie gebruikt wordt. Welk van de twee annotaties je gebruikt, speelt geen rol. Voor versie 3 was de OpenAPI Specification eigendom van Swagger, in 2015 is de specificatie gedoneerd aan de The OpenAPI Initiative. De naam Swagger, wordt nog steeds gebruikt, maar enkel voor de commerciële tools, niet voor de API specificatie.
Om documentatie te schrijven gebruiken we YAML.
Begrip: YAML Syntax
YAML (YAML Ain't Markup Language) is een manier om data te serialiseren op een manier die eenvoudig leesbaar is voor mensen.
De syntax is relatief eenvoudig, YAML bestaat uit key-value. Elk paar begint met de naam van de key, gevolgd door een dubbelpunt, een spatie en tenslotte de value. Deze value kan een string (genoteerd zonder quotes), boolean (genoteerd als true of false) , number, object of array zijn.
Strings values die een dubbelpunt of hekje bevatten, moeten wel tussen quotes gezet worden, anders
key1: De eerste value, geen quotes nodig.
arrayKey1:
- array value 1
- array value 2
multiLineKey1: |
Some multi-line string
Lorem ipsum
multiLineKey2: >
Some other multi-line string
Lorem ipsum
objectKey:
someNestedKey1: some value
someNestedKey2: other value
booleanKey: true
falseBooleanKey: false
numberKey: 42
someStringThatRequiresQuotes: 'Lorem: ipsum'
someStringThatRequiresQuotes2: '#needsQuotes'/docs route
Voor elke route moeten aangeven wat de route en de HTTP-methode is. Verder zijn er heel wat optionele properties.
/**
* @openapi
* /docs:
* get:
* summary: View the docs.
* description: View this documentation
* responses:
* 200:
* description: These docs in HTML format.
*/
docs.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
/**
* @openapi
* /docs.json:
* get:
* summary: Docs in JSON.
* description: This documentation in JSON format.
* responses:
* 200:
* description: This documentation in JSON format.
*/
docs.get('/docs.json', (_: Request, res: Response) => {
res.send(swaggerSpec)
})De website past zich automatisch aan zodra deze doc-strings gedefinieerd zijn. Merk op dat de tekst die we in summary meegegeven hebben op onderstaand screenshot in een zwart kader weergegeven is en de beschrijving in de body van het accordion item.

In bovenstaand screenshot zijn de routes geplaatst onder de default heading, voorlopig is dit geen probleem, maar zodra er meerdere routes gebruikt worden, wordt het snel onoverzichtelijk. Daarom voegen we tags toe aan elke route.
/**
* @openapi
* /docs:
* get:
* tags:
* - Documentation
* summary: View the docs.
* description: View this documentation
* responses:
* 200:
* description: These docs in HTML format.
*/
docs.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
/**
* @openapi
* /docs.json:
* get:
* tags:
* - Documentation
* summary: Docs in JSON.
* description: This documentation in JSON format.
* responses:
* 200:
* description: This documentation in JSON format.
*/
docs.get('/docs.json', (_: Request, res: Response) => {
res.send(swaggerSpec)
})
Query parameters
Als volgende voorbeeld definiëren we de documentatie voor de GET /campuses route. Via de parameters property definiëren we een array van parameters, merk op dat we via in: query aangeven dat deze parameter een query parameter is en dus als ?includeRooms meegegeven moet worden in de URL.
/**
* @openapi
* /campuses:
* get:
* operationId: getCampuses
* tags:
* - Campuses
* summary: Get all campuses.
* description: Retrieve all the Thomas More campuses in the database.
* parameters:
* - in: query
* name: includeRooms
* schema:
* type: boolean
* required: false
* responses:
* 200:
* description: An array of all the campuses in the database, optionally including an array of rooms.
*/
campuses.get('/campuses', async (req: Request<void, Campus[], void, IncludeParams>, res: Response) => {
const {includeRooms} = req.query
const campuses = await getCampuses(includeRooms?.toLowerCase() === 'true')
res.send(campuses)
})Zoals gedemonstreerd wordt in onderstaande video, kan Swagger UI gebruikt worden om de API uit te testen, inclusief de query parameters.
Schema's
Alhoewel de documentatie bruikbaar is, is het veel handiger de data die teruggegeven wordt ook beschreven wordt.
Spijtig genoeg is het niet mogelijk om de TypeScript schema's die gegenereerd worden door Prisma of opgebouwd zijn met Mongoose te gebruiken in de Open API specificaties. In plaats daarvan moeten we de schema's (beschrijvingen van datatypes) ook definiëren via doc-strings. Dit is natuurlijk niet ideaal omdat het schema twee keer genoteerd moet worden en omdat deze twee notaties in sync gehouden moeten worden.
Via de required property geven we alle properties aan die verplicht ingevuld moeten worden. Via properties moeten alle properties opgelijst worden, inclusief diegene die ook al vermeld zijn via de required property. Via de example key die optioneel meegegeven kan worden voor elke property, kan een voorbeeldwaarde meegegeven worden. Deze voorbeeldwaarde wordt dan ook getoond in Swagger UI.
Eens het schema beschreven is, kan dit geïmporteerd worden in
/**
* @openapi
* components:
* schemas:
* Campus:
* type: object
* required:
* - id
* - name
* - location
* - address
* - image
* properties:
* id:
* type: string
* example: 12e4090a-ce57-490b-9582-b1f4a6a1ee4e
* name:
* type: string
* example: Geel
* location:
* type: string
* example: Geel
* address:
* type: string
* example: Kleinhoefstraat 4 2440 Geel
* image:
* type: string
* example: https://thomasmore.be/sites/default/files/styles/text_w_image_width/public/2023-04/369_CampusGeel_2048x1536.jpg?itok=gqP9t8ya
*//**
* @openapi
* /campuses:
* get:
* operationId: getCampuses
* tags:
* - Campuses
* summary: Get all campuses.
* description: Retrieve all the Thomas More campuses in the database.
* parameters:
* - in: query
* name: includeRooms
* schema:
* type: boolean
* required: false
* responses:
* 200:
* description: An array of all the campuses in the database, optionally including an array of rooms.
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Campus'
*/
campuses.get('/campuses', async (req: Request<void, Campus[], void, IncludeParams>, res: Response) => {
const {includeRooms} = req.query
const campuses = await getCampuses(includeRooms?.toLowerCase() === 'true')
res.send(campuses)
})Notitie
Soms is het nodig om de development server opnieuw te starten voordat de wijzigingen in een schema zichtbaar zijn.

Overerving
We hebben niet enkel een schema nodig voor een Campus, maar ook een schema voor lokalen (Room).
/**
* @openapi
* components:
* schemas:
* Campus:
* ...
* Room:
* type: object
* required:
* - id
* - name
* - capacity
* properties:
* id:
* type: string
* example: 12e4090a-ce57-490b-9582-b1f4a6a1ee4e
* name:
* type: string
* example: D101
* capacity:
* type: integer
* example: 36
*/Deze twee schema's kunnen vervolgens gecombineerd worden om een CampusWithRooms schema te maken. Via de allOf property combineren we verschillende schema's, eerst gebruiken we het Campus schema en vervolgens bouwen we een nieuw object waarin we een rooms property definiëren die een array van Room objecten bevat.
/**
* @openapi
* components:
* schemas:
* Campus:
* ...
* Room:
* ...
* CampusWithRooms:
* allOf:
* - $ref: '#/components/schemas/Campus'
* - type: object
* required:
* - rooms
* properties:
* rooms:
* type: array
* items:
* $ref: '#/components/schemas/Room'
*/Tenslotte kunnen voor de GET /campuses route, via de anyOf property, aangeven dat de route ofwel een array van Campus objecten of een array van CampusWithRoom objecten teruggeeft.
/**
* @openapi
* /campuses:
* get:
* operationId: getCampuses
* tags:
* - Campuses
* summary: Get all campuses.
* description: Retrieve all the Thomas More campuses in the database.
* parameters:
* - in: query
* name: includeRooms
* schema:
* type: boolean
* required: false
* responses:
* 200:
* description: |
* An array of all the campuses in the database,
* optionally including an array of rooms.
* content:
* application/json:
* schema:
* type: array
* items:
* anyOf:
* - $ref: '#/components/schemas/Campus'
* - $ref: '#/components/schemas/CampusWithRooms'
*
*/
campuses.get('/campuses', async (req: Request<void, Campus[], void, IncludeParams>, res: Response) => {
const {includeRooms} = req.query
const campuses = await getCampuses(includeRooms?.toLowerCase() === 'true')
res.send(campuses)
})
Request body
Voor het volgende voorbeeld voegen we een beschrijving van het request body voor POST /campuses toe. We moeten opnieuw een beschrijving van het schema toevoegen, vervolgens kunnen we dit schema gebruiken als referentie voor het request body. Merk op dat hieronder markdown gebruikt wordt voor de description properties.
/**
* @openapi
* /campuses:
* post:
* tags:
* - Campuses
* summary: Create a new campus.
* description: Create a new campus.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateCampusInput'
* responses:
* 200:
* description: >
* The newly created `Campus` object, including the `id`.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Campus'
* 400:
* description: >
* <p>Bad Request:</p>
* <br/>
* <p>Returned when a required parameter isn't passed or its length is 0.</p>
*
*/
campuses.post('/campuses', async (req: Request<void, Campus, IdOptional<Campus>>, res: Response) => {
// ...
})/**
* @openapi
* components:
* schemas:
* Campus:
* ...
* Room:
* ...
* CampusWithRooms:
* ...
* CreateCampusInput:
* type: object
* required:
* - name
* - location
* - address
* properties:
* name:
* type: string
* location:
* type: string
* address:
* type: string
* image:
* type: string
* default: https://placehold.co/600x400
*/
Parameters
De laatste route die we bespreken is GET /campus/:id, de definitie voor deze route is bijna exact gelijk aan de vorige routes, het enige verschil is dat we een URL-parameter moeten toevoegen in het pad. Dit kan door het dynamische deel van het pad te omringen met accolades. Vervolgens moeten we via het parameters attribuut de URL parameter definiëren, in tegenstelling tot de vorige parameter, moet er dit keer een path parameter gebruikt worden in de plaats van een query parameter.
/**
* @openapi
* /campuses/{campusId}:
* get:
* operationId: getCampus
* tags:
* - Campuses
* summary: Get a single `Campus` by the `campusId`.
* parameters:
* - in: path
* name: campusId
* description: The id of the campus.
* required: true
* responses:
* 200:
* description: Successful retrieved the `Campus`.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Campus'
* 404:
* description: Campus not found.
*/
Postman
Elke API waarvoor een Open API Specification beschikbaar is, kan geïmporteerd worden in Postman, onderstaande video demonstreert dit voor de voorbeeld-API.