Utforma API:er för mikrotjänster
Bra API-design är viktigt i en mikrotjänstarkitektur, eftersom allt datautbyte mellan tjänster sker antingen via meddelanden eller API-anrop. API:er måste vara effektiva för att undvika att skapa trafikknackande I/O. Eftersom tjänster utformas av team som arbetar oberoende av varandra måste API:er ha väldefinierad semantik och versionsscheman, så att uppdateringar inte bryter mot andra tjänster.

Det är viktigt att skilja mellan två typer av API:
- Offentliga API:er som klientprogram anropar.
- Backend-API:er som används för kommunikation mellan tjänster.
Dessa två användningsfall har något annorlunda krav. Ett offentligt API måste vara kompatibelt med klientprogram, vanligtvis webbläsarprogram eller interna mobilappar. I de flesta fall innebär det att det offentliga API:et använder REST över HTTP. För backend-API:erna måste du dock ta hänsyn till nätverksprestanda. Beroende på tjänsternas kornighet kan kommunikation mellan tjänster resultera i mycket nätverkstrafik. Tjänster kan snabbt bli I/O-bundna. Därför blir överväganden som serialiseringshastighet och nyttolaststorlek viktigare. Några populära alternativ till att använda REST över HTTP är gRPC, Apache Avro och Apache Thrift. Dessa protokoll stöder binär serialisering och är vanligtvis mer effektiva än HTTP.
Överväganden
Här är några saker att tänka på när du väljer hur du implementerar ett API.
REST jämfört med RPC. Överväg kompromisserna mellan att använda ett gränssnitt i REST-format jämfört med ett RPC-gränssnitt.
REST modellerar resurser, vilket kan vara ett naturligt sätt att uttrycka din domänmodell. Den definierar ett enhetligt gränssnitt baserat på HTTP-verb, vilket uppmuntrar till utveckling. Den har väldefinierad semantik vad gäller idempotens, sidoeffekter och svarskoder. Och den framtvingar tillståndslös kommunikation, vilket förbättrar skalbarheten.
RPC är mer inriktat på åtgärder eller kommandon. Eftersom RPC-gränssnitt ser ut som lokala metod-anrop kan det leda till att du utformar alltför trafikiga API:er. Det innebär dock inte att RPC måste vara trafikigt. Det innebär bara att du måste vara försiktig när du utformar gränssnittet.
För ett RESTful-gränssnitt är det vanligaste valet REST över HTTP med JSON. För ett RPC-gränssnitt finns det flera populära ramverk, inklusive gRPC, Apache Avro och Apache Thrift.
Effektivitet. Överväg effektivitet vad gäller hastighet, minne och nyttolaststorlek. Vanligtvis är ett gRPC-baserat gränssnitt snabbare än REST via HTTP.
Gränssnittsdefinitionsspråk (IDL). Ett IDL används för att definiera metoder, parametrar och returvärden för ett API. Ett IDL kan användas för att generera klientkod, serialiseringskod och API-dokumentation. IDL:er kan också användas av API-testverktyg som Postman. Ramverk som gRPC, Avro och Thrift definierar sina egna IDL-specifikationer. REST över HTTP har inget standard-IDL-format, men ett vanligt val är OpenAPI (tidigare Swagger). Du kan också skapa en HTTP-REST API utan att använda ett formellt definitionsspråk, men sedan förlorar du fördelarna med kodgenerering och testning.
Serialisering. Hur serialiseras objekt via kabel? Alternativen omfattar textbaserade format (främst JSON) och binära format som protokollbuffert. Binära format är vanligtvis snabbare än textbaserade format. JSON har dock fördelar vad gäller samverkan, eftersom de flesta språk och ramverk stöder JSON-serialisering. Vissa serialiseringsformat kräver ett fast schema och vissa kräver att en schemadefinitionsfil kompileras. I så fall måste du ta med det här steget i byggprocessen.
Ramverk och språkstöd. HTTP stöds i nästan alla ramverk och språk. gRPC, Avro och Thrift har alla bibliotek för C++, C#, Java och Python. Thrift och gRPC stöder också Go.
Kompatibilitet och samverkan. Om du väljer ett protokoll som gRPC kan du behöva ett protokollöversättningslager mellan det offentliga API:et och backend-delen. En gateway kan utföra den funktionen. Om du använder ett tjänstnät bör du överväga vilka protokoll som är kompatibla med tjänstnätet. Linkerd har till exempel inbyggt stöd för HTTP, Thrift och gRPC.
Vår baslinjerekommendation är att välja REST över HTTP om du inte behöver prestandafördelarna med ett binärt protokoll. REST över HTTP kräver inga särskilda bibliotek. Det skapar minimal koppling eftersom anropare inte behöver en klient-stub för att kommunicera med tjänsten. Det finns omfattande ekosystem med verktyg som stöder schemadefinitioner, testning och övervakning av RESTful HTTP-slutpunkter. Slutligen är HTTP kompatibelt med webbläsarklienter, så du behöver inget protokollöversättningslager mellan klienten och backend-datorn.
Men om du väljer REST över HTTP bör du utföra prestanda- och belastningstestning tidigt i utvecklingsprocessen för att verifiera om den presterar tillräckligt bra för ditt scenario.
RESTful API-design
Det finns många resurser för att utforma RESTful-API:er. Här är några som kan vara till hjälp:
Här är några specifika överväganden att tänka på.
Se upp för API:er som läcker information om intern implementering eller helt enkelt speglar ett internt databasschema. API:et bör modellera domänen. Det är ett kontrakt mellan tjänster och bör helst bara ändras när nya funktioner läggs till, inte bara för att du omstrukturerade viss kod eller normaliserade en databastabell.
Olika typer av klienter, till exempel mobilprogram och webbläsare på skrivbordet, kan kräva olika nyttolaststorlekar eller interaktionsmönster. Överväg att använda mönstret Backends for Frontends för att skapa separata backends för varje klient, som exponerar ett optimalt gränssnitt för klienten.
För åtgärder med sidoeffekter bör du överväga att göra dem idempotenta och implementera dem som PUT-metoder. Detta möjliggör säkra återförsök och kan förbättra återhämtningen. Artikeln Kommunikation mellan tjänster diskuterar det här problemet i detalj.
HTTP-metoder kan ha asynkron semantik, där metoden returnerar ett svar omedelbart, men tjänsten utför åtgärden asynkront. I så fall bör metoden returnera en HTTP 202-svarskod, vilket anger att begäran godkändes för bearbetning, men bearbetningen har ännu inte slutförts. Mer information finns i Asynkront Request-Reply mönster.
Mappa REST till DDD-mönster
Mönster som entitets-, aggregerings- och värdeobjekt är utformade för att placera vissa begränsningar på objekten i din domänmodell. I många diskussioner om DDD modelleras mönstren med hjälp av objektorienterade språkbegrepp (OO) som konstruktorer eller egenskaps-getters och set-metoder. Värdeobjekt ska till exempel vara oföränderliga. I programmeringsspråket OO framtvingar du detta genom att tilldela värdena i konstruktorn och göra egenskaperna skrivskyddade:
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;
}
}
Dessa typer av kodningsmetoder är särskilt viktiga när du skapar ett traditionellt monolitiskt program. Med en stor kodbas kan många undersystem använda objektet, så det är viktigt att Location objektet framtvingar korrekt beteende.
Ett annat exempel är mönstret Lagringsplats, som säkerställer att andra delar av programmet inte gör direkta läsningar eller skrivningar till datalagret:

I en mikrotjänstarkitektur delar tjänsterna dock inte samma kodbas och delar inte datalager. I stället kommunicerar de via API:er. Tänk dig ett fall där Scheduler-tjänsten begär information om en drönare från drönartjänsten. Drönartjänsten har sin interna modell av en drönare, uttryckt via kod. Men Scheduler ser inte det. I stället får den tillbaka en representation av drönarentiteten – kanske ett JSON-objekt i ett HTTP-svar.

Scheduler-tjänsten kan inte ändra drönartjänstens interna modeller eller skriva till drönartjänstens datalager. Det innebär att koden som implementerar drönartjänsten har ett mindre exponerat ytområde jämfört med kod i en traditionell monolit. Om tjänsten Drone definierar en Location-klass är omfånget för den klassen begränsat – ingen annan tjänst använder klassen direkt.
Av dessa skäl fokuserar den här vägledningen inte särskilt mycket på kodningsmetoder eftersom de relaterar till taktiska DDD-mönster. Men det visar sig att du också kan modellera många av DDD-mönstren via REST-API:er.
Till exempel:
Aggregeringar mappar naturligt till resurser i REST. Till exempel exponeras aggregat delivery som en resurs av leverans-API:et.
Aggregeringar är konsekvensgränser. Åtgärder för aggregeringar bör aldrig lämna en aggregering i ett inkonsekvent tillstånd. Därför bör du undvika att skapa API:er som gör att en klient kan ändra det interna tillståndet för en aggregering. I stället prioriterar du grovkorniga API:er som exponerar aggregeringar som resurser.
Entiteter har unika identiteter. I REST har resurser unika identifierare i form av URL:er. Skapa resurs-URL:er som motsvarar en entitets domänidentitet. Mappningen från URL till domänidentitet kan vara täckande för klienten.
Underordnade entiteter i en aggregering kan nås genom att navigera från rotentiteten. Om du följer HATEOAS-principerna kan underordnade entiteter nås via länkar i representationen av den överordnade entiteten.
Eftersom värdeobjekt är oföränderliga utförs uppdateringar genom att ersätta hela värdeobjektet. I REST implementerar du uppdateringar via PUT- eller PATCH-begäranden.
Med en lagringsplats kan klienter fråga, lägga till eller ta bort objekt i en samling och abstrahera information om det underliggande datalagret. I REST kan en samling vara en distinkt resurs med metoder för att fråga samlingen eller lägga till nya entiteter i samlingen.
När du utformar dina API:er bör du tänka på hur de uttrycker domänmodellen, inte bara data i modellen, utan även affärsåtgärderna och begränsningarna för data.
| DDD-begrepp | REST-motsvarighet | Exempel |
|---|---|---|
| Aggregera | Resurs | { "1":1234, "status":"pending"... } |
| Identitet | URL | https://delivery-service/deliveries/1 |
| Underordnade entiteter | Länkar | { "href": "/deliveries/1/confirmation" } |
| Uppdatera värdeobjekt | PUT eller PATCH | PUT https://delivery-service/deliveries/1/dropoff |
| Lagringsplats | Samling | https://delivery-service/deliveries?status=pending |
API-versionshantering
Ett API är ett kontrakt mellan en tjänst och klienter eller användare av den tjänsten. Om ett API ändras finns det risk för att klienter som är beroende av API:et bryts, oavsett om det är externa klienter eller andra mikrotjänster. Därför är det en bra idé att minimera antalet API-ändringar som du gör. Ofta kräver inte ändringar i den underliggande implementeringen några ändringar i API:et. Realistiskt sett vill du dock någon gång lägga till nya funktioner eller nya funktioner som kräver ändring av ett befintligt API.
När det är möjligt kan du göra API-ändringar bakåtkompatibla. Undvik till exempel att ta bort ett fält från en modell, eftersom det kan bryta klienter som förväntar sig att fältet ska finnas där. Att lägga till ett fält bryter inte kompatibiliteten eftersom klienterna bör ignorera alla fält som de inte förstår i ett svar. Tjänsten måste dock hantera det fall där en äldre klient utelämnar det nya fältet i en begäran.
Stöd för versionshantering i ditt API-kontrakt. Om du inför en ny API-ändring introducerar du en ny API-version. Fortsätt att stödja den tidigare versionen och låt klienterna välja vilken version som ska anropas. Det finns ett par olika sätt att göra detta på. En är helt enkelt att exponera båda versionerna i samma tjänst. Ett annat alternativ är att köra två versioner av tjänsten sida vid sida och dirigera begäranden till en eller annan version, baserat på HTTP-routningsregler.
Diagrammet har två delar. "Tjänsten stöder två versioner" visar v1-klienten och v2-klienten som båda pekar på en tjänst. "Sida vid sida-distribution" visar v1-klienten som pekar på en v1-tjänst och v2-klienten som pekar på en v2-tjänst.
Det finns en kostnad för att stödja flera versioner när det gäller utvecklartid, testning och driftkostnader. Därför är det bra att göra gamla versioner inaktuella så snabbt som möjligt. För interna API:er kan teamet som äger API:et arbeta med andra team för att hjälpa dem att migrera till den nya versionen. Det är när en styrningsprocess mellan team är användbar. För externa (offentliga) API:er kan det vara svårare att ta bort en API-version, särskilt om API:et används av tredje part eller av interna klientprogram.
När en tjänstimplementering ändras är det praktiskt att tagga ändringen med en version. Versionen innehåller viktig information vid felsökning av fel. Det kan vara användbart för rotorsaksanalysen att veta exakt vilken version av tjänsten som anropades. Överväg att använda semantisk versionshantering för tjänstversioner. Semantisk versionshantering använder ett STÖRRE. MINDRE. PATCH-format. Klienter bör dock bara välja ett API efter huvudversionsnummer, eller möjligen den lägre versionen om det finns betydande (men icke-större) ändringar mellan mindre versioner. Det är med andra ord rimligt att klienter väljer mellan version 1 och version 2 av ett API, men inte att välja version 2.1.3. Om du tillåter den nivån av kornighet riskerar du att behöva stödja en spridning av versioner.
Mer information om API-versionshantering finns i Versionshantering av ett RESTful-webb-API.
Idempotenta åtgärder
En åtgärd är idempotent om den kan anropas flera gånger utan att ytterligare sidoeffekter produceras efter det första anropet. Idempotens kan vara en användbar motståndskraftsstrategi eftersom det gör att en överordnad tjänst på ett säkert sätt kan anropa en åtgärd flera gånger. En diskussion om den här punkten finns i Distribuerade transaktioner.
HTTP-specifikationen säger att metoderna GET, PUT och DELETE måste vara idempotenta. POST-metoder är inte garanterade att vara idempotenta. Om en POST-metod skapar en ny resurs finns det vanligtvis ingen garanti för att den här åtgärden är idempotent. Specifikationen definierar idempotent på det här sättet:
En begärandemetod anses vara "idempotent" om den avsedda effekten på servern av flera identiska begäranden med den metoden är samma som effekten för en enskild sådan begäran. (RFC 7231)
Det är viktigt att förstå skillnaden mellan PUT- och POST-semantik när du skapar en ny entitet. I båda fallen skickar klienten en representation av en entitet i begärandetexten. Men innebörden av URI:en är annorlunda.
För en POST-metod representerar URI:en en överordnad resurs för den nya entiteten, till exempel en samling. Om du till exempel vill skapa en ny leverans kan URI:en vara
/api/deliveries. Servern skapar entiteten och tilldelar den en ny URI, till exempel/api/deliveries/39660. Den här URI:en returneras i platsrubriken i svaret. Varje gång klienten skickar en begäran skapar servern en ny entitet med en ny URI.För en PUT-metod identifierar URI:en entiteten. Om det redan finns en entitet med den URI:en ersätter servern den befintliga entiteten med versionen i begäran. Om det inte finns någon entitet med den URI:en skapar servern en. Anta till exempel att klienten skickar en PUT-begäran till
api/deliveries/39660. Förutsatt att det inte finns någon leverans med den URI:en skapar servern en ny. Om klienten skickar samma begäran igen ersätter servern den befintliga entiteten.
Här är leveranstjänstens implementering av PUT-metoden.
[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();
}
}
Det förväntas att de flesta begäranden skapar en ny entitet, så metoden anropar optimistiskt på lagringsplatsobjektet och hanterar eventuella dubblettresursundantag genom att CreateAsync uppdatera resursen i stället.
Nästa steg
Lär dig mer om att använda en API-gateway vid gränsen mellan klientprogram och mikrotjänster.