API's ontwerpen voor microservices
Een goed API-ontwerp is belangrijk in een microservicearchitectuur, omdat alle gegevensuitwisseling tussen services gebeurt via berichten of API-aanroepen. API's moeten efficiënt zijn om chatty I/O te voorkomen. Omdat services zijn ontworpen door teams die onafhankelijk van elkaar werken, moeten API's goed gedefinieerde semantiek en versieschema's hebben, zodat updates andere services niet breken.

Het is belangrijk om onderscheid te maken tussen twee typen API's:
- Openbare API's die clienttoepassingen aanroepen.
- Back-end-API's die worden gebruikt voor communicatie tussen de service.
Deze twee use cases hebben enigszins verschillende vereisten. Een openbare API moet compatibel zijn met clienttoepassingen, meestal browsertoepassingen of native mobiele toepassingen. In de meeste tijd betekent dit dat de openbare API REST gebruikt via HTTP. Voor de back-end-API's moet u echter rekening houden met de netwerkprestaties. Afhankelijk van de granulariteit van uw services kan communicatie tussen services leiden tot veel netwerkverkeer. Services kunnen snel I/O-gebonden worden. Daarom worden overwegingen zoals serialisatiesnelheid en payloadgrootte belangrijker. Enkele populaire alternatieven voor het gebruik van REST via HTTP zijn gRPC, Apache Avro en Apache Thrift. Deze protocollen ondersteunen binaire serialisatie en zijn over het algemeen efficiënter dan HTTP.
Overwegingen
Hier zijn enkele dingen om over na te denken bij het kiezen hoe u een API implementeert.
REST versus RPC. Houd rekening met de afweging tussen het gebruik van een REST-interface en een RPC-interface.
REST modelleren resources, wat een natuurlijke manier kan zijn om uw domeinmodel uit te drukken. Het definieert een uniforme interface op basis van HTTP-woorden, waardoor de betaalbaarheid wordt aangemoedigd. Het heeft goed gedefinieerde semantiek op het gebied van idempotentie, neveneffecten en responscodes. Ook wordt staatloze communicatie afgedwongen, waardoor de schaalbaarheid wordt verbeterd.
RPC is meer gericht op bewerkingen of opdrachten. Omdat RPC-interfaces lijken op aanroepen van lokale methoden, kan dit ertoe leiden dat u te veel chatty API's ontwerpt. Dat betekent echter niet dat RPC veel moet doen. Het betekent alleen dat u zorg moet gebruiken bij het ontwerpen van de interface.
Voor een RESTful-interface is REST via HTTP de meest voorkomende keuze met behulp van JSON. Voor een interface in RPC-stijl zijn er verschillende populaire frameworks, waaronder gRPC, Apache Avro en Apache Thrift.
Efficiëntie. Houd rekening met efficiëntie op het gebied van snelheid, geheugen en payloadgrootte. Normaal gesproken is een gRPC-interface sneller dan REST via HTTP.
Interfacedefinitietaal (IDL). Een IDL wordt gebruikt om de methoden, parameters en retourwaarden van een API te definiëren. Een IDL kan worden gebruikt voor het genereren van clientcode, serialisatiecode en API-documentatie. IdL's kunnen ook worden gebruikt door API-testhulpprogramma's zoals Postman. Frameworks zoals gRPC, Avro en Thrift definiëren hun eigen IDL-specificaties. REST via HTTP heeft geen standaard-IDL-indeling, maar een veelgebruikte keuze is OpenAPI (voorheen Swagger). U kunt ook een HTTP-REST API zonder een formele definitietaal te gebruiken, maar dan verliest u de voordelen van het genereren en testen van code.
Serialisatie. Hoe worden objecten geseraliseerd via de kabel? Opties omvatten tekstindelingen (voornamelijk JSON) en binaire indelingen zoals protocolbuffer. Binaire indelingen zijn over het algemeen sneller dan op tekst gebaseerde indelingen. JSON heeft echter voordelen op het gebied van interoperabiliteit, omdat de meeste talen en frameworks ondersteuning bieden voor JSON-serialisatie. Voor sommige serialisatie-indelingen is een vast schema vereist en voor sommige moet een schemadefinitiebestand worden gecompileerd. In dat geval moet u deze stap opnemen in uw buildproces.
Framework- en taalondersteuning. HTTP wordt ondersteund in vrijwel elk framework en elke taal. gRPC, Avro en Thrift hebben allemaal bibliotheken voor C++, C#, Java en Python. Thrift en gRPC ondersteunen ook Go.
Compatibiliteit en interoperabiliteit. Als u een protocol zoals gRPC kiest, hebt u mogelijk een protocolvertalingslaag nodig tussen de openbare API en de back-end. Een gateway kan die functie uitvoeren. Als u een service-mesh gebruikt, moet u overwegen welke protocollen compatibel zijn met de service-mesh. Linkerd heeft bijvoorbeeld ingebouwde ondersteuning voor HTTP, Thrift en gRPC.
Onze basislijnaanbeveling is om REST te kiezen boven HTTP, tenzij u de prestatievoordelen van een binair protocol nodig hebt. VOOR REST via HTTP zijn geen speciale bibliotheken vereist. Er ontstaat minimale koppeling, omdat aanroepers geen client stub nodig hebben om met de service te communiceren. Er zijn uitgebreide ecosystemen met hulpprogramma's voor de ondersteuning van schemadefinities, testen en bewaken van RESTful HTTP-eindpunten. Ten slotte is HTTP compatibel met browserclients, dus u hebt geen protocolvertalingslaag tussen de client en de back-end nodig.
Als u echter REST boven HTTP kiest, moet u vroeg in het ontwikkelingsproces prestatie- en belastingstests uitvoeren om te controleren of deze goed genoeg presteert voor uw scenario.
RESTful API-ontwerp
Er zijn veel resources voor het ontwerpen van RESTful API's. Hier zijn enkele die u mogelijk nuttig vindt:
Hier zijn enkele specifieke overwegingen om rekening mee te houden.
Kijk uit voor API's die interne implementatiedetails lekken of gewoon een intern databaseschema spiegelen. De API moet het domein modelleren. Het is een contract tussen services en zou idealiter alleen moeten veranderen wanneer er nieuwe functionaliteit wordt toegevoegd, niet alleen omdat u code hebt genormaliseerd of een databasetabel hebt genormaliseerd.
Voor verschillende typen client, zoals mobiele toepassingen en desktopwebbrowsers, zijn mogelijk verschillende payloadgrootten of interactiepatronen vereist. Overweeg het gebruik van het patroon Back-enden voor front-enden om afzonderlijke back-enden te maken voor elke client, die een optimale interface voor die client bieden.
Voor bewerkingen met neveneffecten kunt u ze idempotent maken en implementeren als PUT-methoden. Dit maakt veilige nieuwe proberen mogelijk en kan de tolerantie verbeteren. In het artikel Interservicecommunicatie wordt dit probleem gedetailleerder besproken.
HTTP-methoden kunnen asynchrone semantiek hebben, waarbij de methode onmiddellijk een antwoord retourneert, maar de service de bewerking asynchroon wordt uitgevoerd. In dat geval moet de methode een HTTP 202-antwoordcode retourneren, wat aangeeft dat de aanvraag is geaccepteerd voor verwerking, maar de verwerking nog niet is voltooid. Zie AsynchroneRequest-Reply patroon voor meer informatie.
REST toewijzen aan DDD-patronen
Patronen zoals entiteits-, aggregatie- en waardeobjecten zijn ontworpen om bepaalde beperkingen op de objecten in uw domeinmodel te plaatsen. In veel discussies over DDD worden de patronen gemodelleerd met behulp van objectgeoriënteerde (OO)-taalconcepten zoals constructors of eigenschap getters en setters. Waardeobjecten moeten bijvoorbeeld onveranderbaar zijn. In een OO-programmeertaal zou u dit afdwingen door de waarden in de constructor toe te wijzen en de eigenschappen alleen-lezen te maken:
export class Location {
readonly latitude: number;
readonly longitude: number;
constructor(latitude: number, longitude: number) {
if (latitude < -90 || latitude > 90) {
throw new RangeError('latitude must be between -90 and 90');
}
if (longitude < -180 || longitude > 180) {
throw new RangeError('longitude must be between -180 and 180');
}
this.latitude = latitude;
this.longitude = longitude;
}
}
Dit soort coderingsmethoden zijn met name belangrijk bij het bouwen van een traditionele monolithische toepassing. Met een grote codebasis kunnen veel subsystemen het object gebruiken, dus het is belangrijk dat het Location object correct gedrag afdwingt.
Een ander voorbeeld is het opslagplaatspatroon, dat ervoor zorgt dat andere onderdelen van de toepassing geen directe lees- of schrijfgegevens naar het gegevensopslag maken:

In een microservicearchitectuur delen services echter niet dezelfde codebasis en delen ze geen gegevensopslag. In plaats daarvan communiceren ze via API's. Houd rekening met het geval waarin de Scheduler-service informatie over een drone van de Drone Service aanvraagt. De Drone Service heeft een intern model van een drone, uitgedrukt in code. Maar de Scheduler ziet dat niet. In plaats daarvan wordt een representatie van de droneentiteit, mogelijk — een JSON-object in een HTTP-antwoord, teruggehaald.

De Scheduler-service kan de interne modellen van de Drone-service niet wijzigen of schrijven naar het gegevensopslag van de Drone Service. Dit betekent dat de code die de Drone-service implementeert, een kleinere surface area, vergeleken met code in een traditionele monoliet. Als de Drone-service een Locatie-klasse definieert, is het bereik van die klasse beperkt. Geen enkele andere service gebruikt — rechtstreeks de klasse.
Daarom richt deze richtlijn zich niet veel op coderingsmethoden, omdat ze betrekking hebben op de tactische DDD-patronen. Maar het blijkt dat u ook veel van de DDD-patronen kunt modelleren via REST API's.
Bijvoorbeeld:
Statistische gegevens worden op natuurlijke wijze aan resources in REST toe te staan. De Delivery-aggregatie wordt bijvoorbeeld door de Delivery API beschikbaar gemaakt als een resource.
Statistische gegevens zijn consistentiegrenzen. Bewerkingen op statistische gegevens mogen nooit een aggregatie in een inconsistente status laten. Daarom moet u voorkomen dat u API's maakt waarmee een client de interne status van een aggregatie kan bewerken. In plaats daarvan geeft u de voorkeur aan coarse-grain API's die aggregatoren beschikbaar maken als resources.
Entiteiten hebben unieke identiteiten. In REST hebben resources unieke id's in de vorm van URL's. Maak resource-URL's die overeenkomen met de domeinidentiteit van een entiteit. De toewijzing van URL aan domeinidentiteit is mogelijk ondoorzichtig voor de client.
Onderliggende entiteiten van een aggregatie kunnen worden bereikt door te navigeren vanuit de hoofdentiteit. Als u de HATEOAS-principes volgt, kunnen onderliggende entiteiten worden bereikt via koppelingen in de weergave van de bovenliggende entiteit.
Omdat waardeobjecten onveranderbaar zijn, worden updates uitgevoerd door het hele waardeobject te vervangen. Implementeert in REST updates via PUT- of PATCH-aanvragen.
Met een opslagplaats kunnen clients objecten in een verzameling opvragen, toevoegen of verwijderen, waardoor de details van het onderliggende gegevensopslag worden geabstraheerd. In REST kan een verzameling een afzonderlijke resource zijn, met methoden voor het uitvoeren van query's op de verzameling of het toevoegen van nieuwe entiteiten aan de verzameling.
Wanneer u uw API's ontwerpt, moet u bedenken hoe ze het domeinmodel uitdrukken, niet alleen de gegevens in het model, maar ook de bedrijfsactiviteiten en de beperkingen van de gegevens.
| DDD-concept | REST-equivalent | Voorbeeld |
|---|---|---|
| Samenvoegen | Resource | { "1":1234, "status":"pending"... } |
| Identiteit | URL | https://delivery-service/deliveries/1 |
| Onderliggende entiteiten | Koppelingen | { "href": "/deliveries/1/confirmation" } |
| Waardeobjecten bijwerken | PUT of PATCH | PUT https://delivery-service/deliveries/1/dropoff |
| Opslagplaats | Verzameling | https://delivery-service/deliveries?status=pending |
API-versieversies
Een API is een contract tussen een service en clients of consumenten van die service. Als een API wordt gewijzigd, bestaat het risico op het verbreken van clients die afhankelijk zijn van de API, ongeacht of dit externe clients of andere microservices zijn. Daarom is het een goed idee om het aantal DOOR u aangebrachte API-wijzigingen te minimaliseren. Wijzigingen in de onderliggende implementatie vereisen vaak geen wijzigingen in de API. Realistisch gezien wilt u op een bepaald moment echter nieuwe functies of nieuwe mogelijkheden toevoegen waarvoor een bestaande API moet worden veranderd.
Maak, indien mogelijk, API-wijzigingen achterwaarts compatibel. Vermijd bijvoorbeeld het verwijderen van een veld uit een model, omdat dit fouten kan hebben in clients die verwachten dat het veld daar staat. Het toevoegen van een veld verbreekt de compatibiliteit niet, omdat clients alle velden die ze niet in een antwoord begrijpen, moeten negeren. De service moet echter het geval afhandelen waarbij een oudere client het nieuwe veld in een aanvraag weglaten.
Ondersteuning voor versieversies in uw API-contract. Als u een wijziging in de API introduceert die een grote wijziging doorbreekt, introduceert u een nieuwe API-versie. Blijf de vorige versie ondersteunen en laat clients selecteren welke versie moet worden aanroepen. Er zijn een aantal manieren om dit te doen. U hoeft alleen maar beide versies in dezelfde service beschikbaar te maken. Een andere optie is om twee versies van de service naast elkaar uit te voeren en aanvragen te routeren naar een of de andere versie, op basis van HTTP-routeringsregels.
Het diagram heeft twee delen. 'Service ondersteunt twee versies' toont de v1-client en de v2-client die beide naar één service wijzen. 'Side-by-side-implementatie' toont de v1-client die verwijst naar een v1-service en de v2-client die verwijst naar een v2-service.
Er zijn kosten voor het ondersteunen van meerdere versies, wat betreft ontwikkelaarstijd, tests en operationele overhead. Daarom is het goed om oude versies zo snel mogelijk af te handelen. Voor interne API's kan het team dat eigenaar is van de API samenwerken met andere teams om hen te helpen migreren naar de nieuwe versie. Dit is wanneer een governanceproces tussen teams nuttig is. Voor externe (openbare) API's kan het moeilijker zijn om een API-versie af te bouwen, met name als de API wordt gebruikt door derden of door native clienttoepassingen.
Wanneer een service-implementatie wordt gewijzigd, is het handig om de wijziging te taggen met een versie. De versie bevat belangrijke informatie bij het oplossen van fouten. Het kan handig zijn om bij de hoofdoorzaakanalyse precies te weten welke versie van de service is aangeroepen. Overweeg het gebruik van semantic versioning voor serviceversies. Semantic Versioning maakt gebruik van een MAJOR. Kleine. PATCH-indeling. Clients moeten echter alleen een API selecteren op het nummer van de belangrijkste versie, of mogelijk de secundaire versie als er belangrijke (maar niet-belangrijke) wijzigingen tussen secundaire versies zijn. Met andere woorden, het is redelijk voor clients om te kiezen tussen versie 1 en versie 2 van een API, maar niet om versie 2.1.3 te selecteren. Als u dat granulariteitsniveau toestaat, loopt u het risico dat u een toenemend aantal versies moet ondersteunen.
Zie Versioning a RESTful web API (Versie maken van een RESTful-web-API)voor meer informatie over API-versieversies.
Idempotente bewerkingen
Een bewerking is idempotent als deze meerdere keren kan worden aangeroepen zonder extra neveneffecten te produceren na de eerste aanroep. Idempotentie kan een nuttige tolerantiestrategie zijn, omdat hiermee een upstream-service een bewerking veilig meerdere keren kan aanroepen. Zie Gedistribueerde transacties voor een bespreking van dit punt.
De HTTP-specificatie geeft aan dat de methoden GET, PUT en DELETE idempotent moeten zijn. POST-methoden zijn niet gegarandeerd idempotent. Als een POST-methode een nieuwe resource maakt, is er doorgaans geen garantie dat deze bewerking idempotent is. De specificatie definieert idempotent op deze manier:
Een aanvraagmethode wordt beschouwd als 'idempotent' als het beoogde effect op de server van meerdere identieke aanvragen met die methode hetzelfde is als het effect voor één dergelijke aanvraag. (RFC 7231)
Het is belangrijk om het verschil te begrijpen tussen put- en POST-semantiek bij het maken van een nieuwe entiteit. In beide gevallen verzendt de client een weergave van een entiteit in de aanvraag body. Maar de betekenis van de URI is anders.
Voor een POST-methode vertegenwoordigt de URI een bovenliggende resource van de nieuwe entiteit, zoals een verzameling. Als u bijvoorbeeld een nieuwe levering wilt maken, kan de URI
/api/deliverieszijn. De server maakt de entiteit en wijst er een nieuwe URI aan toe, zoals/api/deliveries/39660. Deze URI wordt geretourneerd in de Locatie-header van het antwoord. Telkens als de client een aanvraag verzendt, maakt de server een nieuwe entiteit met een nieuwe URI.Voor een PUT-methode identificeert de URI de entiteit. Als er al een entiteit met die URI bestaat, vervangt de server de bestaande entiteit door de versie in de aanvraag. Als er geen entiteit met die URI bestaat, maakt de server er een. Stel bijvoorbeeld dat de client een PUT-aanvraag naar
api/deliveries/39660verzendt. Ervan uitgaande dat er geen levering met die URI is, maakt de server een nieuwe. Als de client nu dezelfde aanvraag opnieuw verzendt, vervangt de server de bestaande entiteit.
Hier is de implementatie van de PUT-methode van de leveringsservice.
[HttpPut("{id}")]
[ProducesResponseType(typeof(Delivery), 201)]
[ProducesResponseType(typeof(void), 204)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
try
{
var internalDelivery = delivery.ToInternal();
// Create the new delivery entity.
await deliveryRepository.CreateAsync(internalDelivery);
// Create a delivery status event.
var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);
// Return HTTP 201 (Created)
return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
}
catch (DuplicateResourceException)
{
// This method is mainly used to create deliveries. If the delivery already exists then update it.
logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);
var internalDelivery = delivery.ToInternal();
await deliveryRepository.UpdateAsync(id, internalDelivery);
// Return HTTP 204 (No Content)
return NoContent();
}
}
Het is te verwachten dat de meeste aanvragen een nieuwe entiteit maken, dus de methode roept optimistisch aan op het opslagplaatsobject en verwerkt vervolgens eventuele uitzonderingen met dubbele resources door de resource bij te CreateAsync werken.
Volgende stappen
Meer informatie over het gebruik van een API-gateway op de grens tussen clienttoepassingen en microservices.