Introductie
Werken met api's brengt frustraties met zich mee die veel developers zullen herkennen: onduidelijke documentatie, een respons die niet overeenkomt met de beschrijving in de documentatie, of de documentatie nog handmatig moeten updaten nadat nieuwe functionaliteit is toegevoegd. Ook het ontwikkelen van api’s kent de nodige uitdagingen. Veel van deze frustraties en uitdagingen kunnen worden overwonnen met de OpenAPI-specificatie.
De OpenAPI-specificatie (OAS) doet een poging om rest-api's toegankelijker te maken door ze op een gestandaardiseerde manier te beschrijven. Dit stelt mensen en computers in staat om met minimale inspanning een api te gebruiken. De OAS, die vroeger Swagger heette, heeft onlangs veel aan populariteit gewonnen. Het ecosysteem is enorm gegroeid en daardoor zijn er ontzettend veel tools op de markt gekomen die helpen om stabiele api's te ontwikkelen en te gebruiken. Dat is ook de Nederlandse overheid niet ontgaan. Zij was een early adopter en voegde de OAS al in mei 2018 toe aan haar open ict-standaarden.
Hoe werkt de OpenAPI-specificatie?
Met de OpenAPI-specificatie beschrijf je een api in een OpenAPI-document. Een OpenAPI-document kan worden geschreven in twee verschillende formaten: JSON en YAML.
Een minimaal OpenAPI-document in JSON:
Hetzelfde OpenAPI-document in YAML:
Allereerst beschrijven we de versie van de OpenAPI-specificatie die we willen gebruiken. In dit geval definiëren we versie 3.0.0. Zo kunnen we andere tools vertellen welke versie we gebruiken, zodat ze het document op de juiste manier kunnen interpreteren. Vervolgens is basale informatie uitgewerkt in het info-object. De naam van onze api, de huidige versie en een omschrijving.
Het paths-object is op dit moment nog leeg. In het paths-object kunnen operaties worden gespecificeerd. Hier wordt uiteindelijk beschreven wat de nieuwe api allemaal kan. Omdat YAML iets makkelijker leesbaar is, houden we dit formaat voor de rest van dit artikel aan. Er bestaan converters die moeiteloos documenten naar een ander formaat transformeren.
Operaties
Alles wat je kunt met een api, beschrijf je in een operatie. In OpenAPI bestaan die uit twee delen: een path en een http-verb. Stel je voor dat we via onze api een lijst met gebruikers willen ontsluiten. Een logische keus voor een operatie zou in dit geval een get-request zijn op het /users-path:
De operatie heeft nu wel een duidelijke omschrijving, maar het belangrijkste ontbreekt nog: wat wordt er teruggestuurd? Dit kunnen we definiëren in het responses-object. Dat bevat een lijst met objecten die als key een http-statuscode hebben. Een succesvolle response op een get-request heeft normaliter de statuscode 200.
Hoewel deze beschrijving vertelt hoe een lijst met gebruikers kan worden opgehaald, is nog onbekend wat er wordt teruggestuurd en welke contenttypes, zoals JSON of XML, ondersteund worden. Door een contentobject toe te voegen aan de beschrijving van de ‘200’-response, kan per contenttype een structuur worden gedefinieerd.
Om de datastructuur te beschrijven, wordt gebruikgemaakt van het Schema Object, dat is geïnspireerd op JSON Schema. Voor ons voorbeeld kiezen we ervoor om de output een array te laten zijn van gebruikers. Iedere gebruiker heeft een unieke username.
Een response op een request naar GET /users zou er volgens onze beschrijving als volgt uitzien:
Design first versus code first
Tot nu toe hebben we gewerkt vanuit de 'design first'-filosofie. Voordat we de code hebben geschreven, is gespecificeerd hoe onze api zou moeten werken. Het grootste voordeel hiervan is dat je de requirements van een api kunt uitwerken voordat je werkt aan een implementatie. Dit geeft gebruikers van de api de kans om alvast te werken aan hun kant van de implementatie. Dit kan omdat zij aan de hand van het OpenAPI-document weten welke endpoints ze moeten aanroepen en welke responses ze kunnen verwachten.
Het tegenovergestelde van ‘design first’ is 'code first'. Hier werk je eerst de code uit en beschrijf je achteraf pas hoe je datastructuur eruitziet. Op die manier kunnen ook bestaande api’s met terugwerkende kracht worden beschreven met de OAS. Voorstanders van ‘code first’ geven vaak als argumenten dat ze zo sneller kunnen ontwikkelen en dat het niet zoveel uitmaakt dat de documentatie later wordt geschreven. Het is echter twijfelachtig of dit ook in de praktijk opgaat.
Achteraf documentatie genereren is lastig en tijdrovend
De feedbackloop vindt bij ‘code first’ later plaats dan bij ‘design first’. Dit houdt in dat de consumenten van de api pas feedback kunnen geven als de code al geschreven is. Eventuele wijzigingen worden zo al snel kostbaar; de wijzigingen moeten getest worden en opnieuw worden gedeployed. Gebruikers moeten de wijzigingen vervolgens zelf testen en kunnen weer met nieuwe feedback komen. Met een ‘design first’-aanpak is dit veel goedkoper. De specificaties kunnen worden gedeeld met de consument voordat er een regel code is geschreven. Als de specificatie is beschreven met OAS, kan de integratie zelfs al getest worden door middel van een mockserver. Dat is een speciale server die dummydata teruggeeft aan de hand van een OpenAPI-specificatie.
“We schrijven de documentatie later wel” betekent in de praktijk vaak: “We schrijven geen documentatie”. Achteraf documentatie genereren is lastig en tijdrovend. In de code moet worden gezocht welke operaties allemaal mogelijk zijn en hoe de bijbehorende requests en responses eruit moeten zien. En met een beetje pech komen er tijdens het ontwikkelen van de documentatie nog nieuwe featurerequests binnen, waardoor de nieuwe documentatie nog steeds niet overeenkomt met de code.
Automatisch documentatie genereren
Om gebruikers de mogelijkheden van een api te laten ontdekken, is het mogelijk om overzichtelijke documentatie te genereren aan de hand van een OpenAPI-document. Er zijn diverse tools in het OpenAPI-ecosysteem die daar ondersteuning voor bieden. Deze tools lezen het OpenAPI-document uit en kunnen zodoende alle mogelijke operaties overzichtelijk weergeven. Voor dit voorbeeld wordt gebruikgemaakt van Redoc, met ruim elfduizend stars op Github een van de populairste libraries met betrekking tot het genereren van documentatie op basis van OAS. Het wordt onder andere gebruikt om de Docker Engine API te documenteren.
Redoc is gebouwd in React. Een van de manieren om het te gebruiken is door de React-library in te laden via een cdn. Voor deze demo heb ik de specificatie geüpload als Github-gist. Onderstaand HTML-document is voldoende om onze documentatie te renderen in een browser.
Het resultaat ziet er zo uit in de browser:
/i/2004130388.png?f=imagenormal)
Mockservers
Nu consumenten toegang hebben tot het OpenAPI-document, kunnen ze tools gebruiken om de integratie met een api zo makkelijk mogelijk te maken. Een van de manieren om de integratie te testen, is door een mockserver te gebruiken. Een mockserver ontvangt een request van een client en kijkt of het request gematcht kan worden met een van de beschreven operaties.
In dit geval wordt onze zojuist beschreven operatie opgevraagd en wenst de client een response te ontvangen in JSON. De mockserver leest in het OpenAPI-document uit wat de structuur moet zijn van de data en geeft een gefabriceerde response met dummydata terug.
/i/2004125448.png?f=imagenormal)
Een van de populairste mockservers is Prism. Het is gebouwd als NodeJS-applicatie, leest OpenAPI-documenten uit en transformeert ze in een mockserver. Onderstaand commando start een mockserver van onze demo-api. De default host is 127.0.0.1:4010.
Als alles correct verlopen is, laat Prism weten dat de server gestart is en welke operaties beschikbaar zijn.
Als je via de browser of een http-client http://localhost:4010/users bezoekt, geeft de mockserver een response terug aan de hand van onze OpenAPI-specificatie. Mijn response zag er zo uit:
Toekomstige gebruikers van onze api kunnen nu alvast de implementatie aan hun kant doen en hoeven uiteindelijk alleen de URL te veranderen in de URL waar de daadwerkelijke implementatie komt te staan. Hierdoor kunnen wij rustig verder werken aan onze eigen implementatie en hoeven onze gebruikers niet op ons te wachten. Tijdwinst!
Validatie
Een OpenAPI-document kan ook helpen met het specificeren van validatieregels voor data. Het valideren van data in een applicatie verloopt in twee stappen: het valideren van de datastructuur van de ingestuurde data en het valideren van de correctheid van deze data.
Tijdens het valideren van de datastructuur wordt gekeken of alle benodigde data beschikbaar is en of die in de juiste structuur is aangeleverd om een operatie voort te zetten. Stel je voor dat een api een operatie ondersteunt die het mogelijk maakt om een nieuwe gebruiker te registreren op basis van een unieke gebruikersnaam en een veilig wachtwoord van minimaal acht tekens. Op basis van de ruwe data kunnen we het request al valideren op diverse aspecten:
- Zijn in het request een gebruikersnaam en een wachtwoord meegestuurd?
- Zijn beide waarden van het datatype 'string'?
- Is het wachtwoord minimaal acht tekens lang?
Is er een aspect waaraan de data niet voldoet, dan kan de validatie vroegtijdig falen. Api's geven in dit geval vaak een response terug met de 422 Unprocessable Entity-statuscode.
Er is echter nog één ander scenario dat nu niet gevalideerd is: de uniciteit van de gebruikersnaam. Om dit te kunnen valideren, moet worden gekeken of de ingevoerde gebruikersnaam voorkomt in een lijst met al geregistreerde gebruikers. Omdat dit niet kan op basis van enkel de ingevoerde data van de gebruiker, valt het in de tweede categorie: het valideren van de correctheid van de data. Vanwege de afhankelijkheid van externe data kan een OpenAPI-document hier niet bij helpen. Alle andere validatie kan plaatsvinden door de data te beschrijven.
Om deze validatieregels in het OpenAPI-document te verwerken, moet ook het Request worden beschreven. Dat gebeurt net als bij de Response met een Schema Object:
In traditionele applicaties die niet beschreven zijn met een OpenAPI-document, moeten de validatieregels worden gedupliceerd. Aan de kant van de client en aan de kant van de server moet dezelfde datastructuur worden gevalideerd. Door OpenAPI-validators in te zetten kan het proces van validatie van de datastructuur volledig worden geautomatiseerd. Zo kunnen api’s ervoor kiezen om een request alleen te accepteren als de validatie op basis van het OpenAPI-document slaagt. Api’s kunnen bijvoorbeeld proxymiddleware gebruiken die de validatie doet op basis van een OpenAPI-document en een http-request. Voordat het request naar de api gaat, belandt het in deze proxy. De proxy valideert dit request vervolgens. Als de validatie slaagt, wordt het request doorgegeven aan de api om te worden afgehandeld. Slaagt de validatie niet, dan stuurt de proxy een foutmelding terug en belandt het request niet bij de api. De onderstaande afbeelding illustreert hoe validatie op basis van proxymiddleware werkt.
/i/2004125444.png?f=imagearticlefull)
Zoals te zien in bovenstaande afbeelding wordt een request gestuurd met de intentie om een nieuwe gebruiker aan te maken. Voordat het request naar de applicatie gaat, komt het langs de proxymiddleware. Deze middleware zoekt het schema voor de requestbody op aan de hand van de operatie (POST /users). In het voorbeeld heeft de gebruiker een wachtwoord opgegeven (“short”) dat minder lang is dan de minimale vereiste van acht tekens. De proxymiddleware grijpt in door verdere afhandeling af te breken en laat de server een response (422 Unprocessable Entity) terugsturen met daarin een gedetailleerde foutmelding.
Sommige OpenAPI-validators kunnen worden gebruikt in de applicatie zelf. In dat geval roept de applicatie zelf de validator aan. De principes blijven gelijk. De validator ontvangt het request en bepaalt aan de hand van het gedefinieerde schema of de datastructuur voldoet. De applicatie kan dan vervolgens zelf besluiten om het request voort te zetten of af te breken op basis van het resultaat van de validatie.
Welke manier je ook gebruikt om je data te valideren op basis van een OpenAPI-document, het resultaat is dat de datastructuur geautomatiseerd wordt gevalideerd zonder dat een applicatie deze regels hoeft te dupliceren.
Je kunt een OpenAPI-specificatie zien als een contract tussen leverancier en consument
Als het OpenAPI-document wordt gebruikt als source of truth voor de validatieregels, wordt het wijzigen van regels versimpeld. Stel je voor dat om veiligheidsredenen het minimale aantal tekens van een wachtwoord van acht naar twaalf gaat. Door enkel het juiste schema aan te passen in het OpenAPI-document is deze regel doorgevoerd voor zowel de client als de server en is ook meteen je api-documentatie aangepast.
Het valideren van data op basis van een OpenAPI-document geldt niet alleen voor requests, maar ook voor responses. Je kunt een OpenAPI-specificatie zien als een contract tussen leverancier en consument. Om te voorkomen dat een api een response teruggeeft die niet overeenkomt met de OpenAPI-specificatie, kan de response worden gevalideerd op de manier waarop ook een request wordt gevalideerd. De daadwerkelijke response wordt dan vergeleken met de response die in het OpenAPI-document wordt beschreven. Dit kan bijvoorbeeld in de vorm van geautomatiseerde tests. Deze tests sturen requests naar de api en valideren of de responses overeenkomen met de specificatie.
Een andere optie is het introduceren van middleware die de response valideert. Tijdens het ontwikkelen van de api kan deze middleware een foutmelding geven wanneer de response afwijkt van de specificatie. In een productieomgeving zou iedere afwijking gelogd kunnen worden.
Overige toepassingen
Stoplight Studio - Grafische interface om api's te ontwerpen
Niemand houdt ervan om grote documenten te schrijven in JSON of YAML; het is foutgevoelig en kost veel tijd. Grafische interfaces als Stoplight Studio lossen die problemen op. Stoplight Studio is een api-designtool die het genereren van een OpenAPI-document makkelijker en sneller maakt.
/i/2004125442.png?f=imagenormal)
Bovendien biedt Stoplight Studio nog enkele andere voordelen. Zo kun je makkelijk met verschillende mensen samenwerken en push je nieuwe changes vanuit de gui naar sourcecontrol. Ook heeft Stoplight Studio een ingebouwde mockserver en een linter om consistentie af te dwingen.
Nog meer toepassingen
Het automatisch genereren van documentatie, het inzetten van mockservers en het automatisch valideren van requests zijn slechts enkele voorbeelden van hoe de OpenAPI-specificatie kan helpen bij het standaardiseren van api’s. Er zijn nog een hoop andere toepassingen van de OpenAPI-specificatie, zoals contracttesting en het automatisch genereren van sdk’s. Op https://openapi.tools is een lange lijst te vinden met tools die kunnen helpen om het maximale uit een OpenAPI-document te halen.
Tutorial
Om te illustreren hoe een OpenAPI-specificatie van waarde kan zijn in een echte applicatie, heb ik de theorie die in dit artikel is besproken, verwerkt in een api. De broncode van deze api heb ik geüpload op GitHub. Als je het leuk vindt om je handen vuil te maken, kun je de broncode downloaden en de api op je eigen computer draaien. Het vereist wel wat kennis van de commandline. Op de GitHub-pagina wordt uitvoerig beschreven hoe je dit voor elkaar krijgt. Als je alle stappen volgt, heb je lokaal een api, een mockserver en de bijbehorende api-documentatie tot je beschikking. Hieronder volgt een tutorial waarin de api ontdekt en verder ontwikkeld wordt.
De infrastructuur
De basis van de api is uiteraard de OpenAPI-specificatie. De specificatie is opgeslagen in reference/demo-api.v1.yaml. Alle tools maken gebruik van dit bestand als basis voor hun functionaliteit.
/i/2004125440.png?f=imagenormal)
De api-documentatie bekijken
Als je een lokale omgeving hebt gestart, is de api-documentatie (Redoc) te bekijken op http://localhost:8080. Hier kun je zien dat er momenteel drie endpoints beschreven zijn:
- GET /users - Een lijst met gebruikers opvragen
- POST /users - Een nieuwe gebruiker registreren
- GET /users/{username} - Een individuele gebruiker opvragen
Requests maken
Requests kunnen gedaan worden aan zowel de mockserver (http://localhost:3100) als de api-server (http://localhost). Als voorbeeld wordt gebruikgemaakt van cURL, een commandlinetool die http-requests kan maken. Je kunt ook gebruikmaken van een http-client met een grafische interface, zoals Postman. Een voordeel van een http-client als Postman is dat je de OpenAPI-specificatie kunt importeren, zodat alle requests geconfigureerd worden.
Om een lijst met gebruikers op te halen via de mockserver, maken we het volgende request:
De response zal er zo uitzien:
Prism genereert een response op basis van het schema van het OpenAPI-document. Het is mogelijk om meer dynamische en realistische data op te vragen door een Prefer-header mee te sturen.
De response wordt dan gegenereerd met dynamische voorbeelden.
De api-server kunnen we op dezelfde manier raadplegen.
Het resultaat is een lege array. Op dit moment is er nog geen enkele gebruiker geregistreerd.
Een nieuwe gebruiker registreren
Zoals beschreven in de OpenAPI-specificatie kunnen we een nieuwe gebruiker registreren door een POST-request te doen aan het /users-endpoint met daarin een gebruikersnaam en wachtwoord van minimaal acht tekens.
Zowel de mockserver als de api-server maakt gebruik van validatie op basis van het OpenAPI-document. Om te verifiëren dat beide servers dat doen, kunnen we een request sturen met verkeerde data.
In het eerste voorbeeld sturen we een te kort wachtwoord mee.
Hoewel de responsebody niet volledig overeenkomt tussen beide servers, geven ze beide een 422 Unprocessable Entity-statuscode terug met de correcte foutmelding.
Een request dat overeenkomt met het gedefinieerde schema, geeft wel de verwachte response terug met een 201 Created-statuscode:
Als je het request herhaalt, stuit je op het essentiële verschil tussen een mockserver en de daadwerkelijke api-server. Bij zowel de mockserver als de daadwerkelijke api-server slaagt de requestvalidatie. De mockserver geeft opnieuw een succesvolle response, maar de api-server faalt. Dit gebeurt doordat de api-server naast schemavalidatie ook nog businessregels valideert. Zoals eerder uitgelegd, kunnen deze regels niet door middel van een OpenAPI-document worden beschreven. Dit is de reden dat je niet je volledige validatie kunt doen op basis van een OpenAPI-document.
Het OpenAPI-document aanpassen
Verander in reference/demo-api.v1.yaml op regel 95 de minLength van het wachtwoord naar 12 en sla vervolgens het document op.
Als je de documentatiepagina ververst, is direct te zien dat de validatieregel is aangepast naar de nieuwe waarde. Ook de mockserver en de api-server hebben de nieuwe regel direct geadopteerd.
Om dat te bewijzen, sturen we nogmaals een request naar het POST /users-endpoint met een wachtwoord van elf tekens.
Dit is de daadwerkelijke kracht van de OpenAPI-specificatie. Wijzigingen worden doorgevoerd in al onze services vanuit een OpenAPI-document.
Een nieuw endpoint toevoegen
Als laatste onderdeel van deze tutorial zetten we een nieuw endpoint op om informatie op te vragen over de auteur van de api. Het doel is om een endpoint op te zetten waarmee je via een get-request op /author informatie over jezelf teruggeeft.
Vanuit de 'design first'-filosofie specificeren we eerst hoe de response eruit moet zien. In dit voorbeeld geven we onze naam en ons e-mailadres terug. Laten we dit vastleggen in het OpenAPI-document (reference/demo-api.v1.yaml).
Om te testen of het request er goed uitziet, sturen we een http-request naar het nieuwe endpoint via de mockserver.
Als laatste stap moeten we het endpoint nog implementeren in de daadwerkelijke api. Voeg de volgende codesnippet toe aan public/index.php boven het $app->run()-statement.
Maak vervolgens een request aan het nieuwe endpoint.
Als alles goed is gegaan, geeft de api nu de response terug die jij hebt geconfigureerd.
Dit was het laatste onderdeel van deze tutorial. Hiermee heb ik geprobeerd een zo goed mogelijke introductie te geven van de OpenAPI-specificatie. Mocht je desondanks nog vragen hebben, laat het dan weten in de comments. Ik zal proberen de meeste vragen te beantwoorden.