RESTful API-tiptips fra erfaring

En arbejdsguide med API-designtip og trendevalueringer.

© Nathaniel Merz - Imaginary Peaks via. 500px
  • Jeg har migreret de nyeste versioner af denne artikel til min GitHub!
  • https://github.com/ptboyer/restful-api-design-tips
  • ️ Du er velkommen til at læse den der, og forlade en stjerne, hvis du nød det!
  • Sidst opdateret den 9. juni 2019.
Vi er alle lærlinger i et håndværk, hvor ingen nogensinde bliver en mester.

Når jeg skriver dette, humrer jeg til mig selv ved at se en stor parallel bag mig selv, der henviser til Hemingways citat fra en anden; den rene opfattelse af, at jeg ikke behøver at arbejde på at skabe en anden implementering af passagen med lignende funktionalitet for resultatværdien (eller i dette tilfælde betydning), er et litterært bevis på genbrug af kode!

Men jeg er ikke her for at skrive om fordelene ved kodepakker, men mere for at nævne nogle af de træk, jeg er blevet værdsat og aktivt implementere i nuværende og fremtidige projekter. Og med disse funktioner og implementeringsdetaljer, udvikler jeg min egen pakke med API-regler og primitiver.

Fra offentliggørelsen af ​​denne artikel har mange diskussionstråde i kanaler som Reddit hjulpet mig med at justere og finpusse nogle af mine forklaringer og holdninger til API-design. Jeg vil gerne takke alle, der har bidraget til diskussionen, og jeg håber, at dette hjælper med at opbygge denne artikel til en mere værdifuld ressource for andre! (Redigering: 9 / juni / 2019) Og nu er det to år siden jeg først offentliggjorde denne artikel, og det har været utroligt at se, at den er blevet set 150.000+ gange og modtaget tusinder af likes og delinger, og endnu en gang vil jeg udtrykke min taknemmelighed til alle mine læsere og tilhængere!

Versionering

Hvis du vil udvikle en API til enhver klienttjeneste, vil du gerne forberede dig til eventuel ændring. Dette realiseres bedst ved at tilvejebringe et "version-namespace" til dit RESTful API.

Vi gør dette ved blot at tilføje versionen som præfiks til alle webadresser.

FÅ www.myservice.com/api/v1/posts

Ved at studere andre API-implementeringer er jeg dog vokset til at kunne lide en kortere URL-stil, der tilbydes ved at få adgang til API'en som en del af et underdomæne og derefter droppe / api fra ruten; kortere og mere kortfattet er bedre.

FÅ api.myservice.com/v1/posts

Cross-Origin Resource Sharing (CORS)

Det er vigtigt at overveje, at når du placerer din API i et andet underdomæne, f.eks. Api.myservice.com, vil det kræve implementering af CORS til din backend, hvis du planlægger at være vært for dit frontend-websted på www.myservice.com og forventer at bruge hentningsanmodninger uden At kaste Ingen adgangskontrol-Tillad-oprindelseshoved er aktuelle fejl.

Ruter

Når du bygger dine ruter, skal du tænke på dine slutpunkter som grupper af ressourcer, som du kan læse, tilføje, redigere og slette fra, og disse handlinger er indkapslet som HTTP-metoder.

Brug HTTP-metoder

Brug metoder såsom:

  • FÅ til hentning af data.
  • POST til tilføjelse af data.
  • PUT til opdatering af data (som et samlet objekt).
  • PATCH til opdatering af data (med delvis information om objektet).
  • Slet for at slette data.

Jeg vil gerne tilføje, at jeg synes, PATCH er en fantastisk måde at skære ned på størrelsen på anmodninger om at ændre dele af større objekter, men også at det passer godt til almindeligt implementerede felter med automatisk indsendelse / auto-gemme.

Et dejligt eksempel på dette er med Tumblrs "Dashboard-indstillinger" -skærm, hvor ikke-kritiske indstillinger om brugeroplevelsen af ​​tjenesten kan redigeres og gemmes pr. Element uden behov for en endelig formular-indsendelsesknap. Det er simpelthen en meget mere organisk måde at interagere med brugerens præferencesdata på.

Mærket “Gemt” vises og forsvinder derefter kort efter ændring af indstillingen.

Brug flertal

Det giver semantisk mening, når du anmoder om mange indlæg fra / indlæg.

Og af hensyn til godheden må du ikke overveje / sende / alle med / post /: id!

// DO: flertalsformer er konsistente og giver mening
GET / v1 / posts /: id / vedhæftede filer /: id / comments

// IKKE: er det kun en kommentar? er det en form? etc.
GET / v1 / post /: id / vedhæftet fil /: id / comment

I tilfælde som disse skal du blot prøve at komme så tæt på flertal som du kan!

"Jeg kan godt lide ideen om at bruge flertalsformer til ressourcenavne, men nogle gange får du navne, der ikke kan pluraliseres." (Kilde)

Brug reden til filtrering af forhold

Forespørgselsstrenge skal bruges til yderligere filtrering af resultater ud over den oprindelige gruppering af et logisk sæt, der tilbydes af et forhold.

Formål at designe slutpunktsstier, der undgår unødvendige forespørgselsstrengsparametre, da de generelt er sværere at læse og arbejde med sammenlignet med stier, hvis struktur fremmer en indledende relationsbaseret filtrering og gruppering af sådanne emner, jo dybere det går.

Dette / indlæg / x / vedhæftede filer er bedre end / vedhæftede filer? PostId = x. Og dette / posts / x / vedhæftede filer / y / comments er så meget bedre end / comments? PostId = x & attachmentId = y.

Brug mere af din "Rute-plads"

Du skal sigte mod at holde din API så flad som muligt og ikke forkaste dine ressourcestier. Tillad dig selv at give flade ruter til alle opdatering / slet dine ressourcer, f.eks. I tilfælde af, at indlæg har kommentarer, lad / indlæg /: id / kommentarer hente kommentarer til et indlæg baseret på forhold, men tilbyder også / kommentarer /: id at tillade redigering af kommentarer uden at have brug for et håndtag til indlægget for hver enkelt rute.

  • Længere stier til at oprette / hente indkapslede ressourcer efter forhold
  • Kortere stier til opdatering / sletning af ressourcer baseret på deres id.

Brug autorisationskonteksten til at filtrere

Når det kommer til at give et slutpunkt for at få adgang til alle en brugers egne ressourcer (f.eks. Alle mine egne indlæg) kan du muligvis ende med mange måder at give disse oplysninger på, det er op til dig, hvad der bedst passer til din applikation.

  1. Nest a / posts-forhold under / mig med GET / me / posts, eller
  2. Brug det eksisterende / stillinger endpoint, men filtrer med forespørgselsstreng, GET / posts? User = , eller
  3. Genanvend / indlæg for kun at vise dine egne indlæg, og udsæt offentlige indlæg med GET / feed / posts.

Brug et "mig" slutpunkt

Har et slutpunkt som GET / mig til at levere grundlæggende data om brugeren, som adskilles gennem autorisationshovedet. Dette kan omfatte info om brugerens tilladelser / scopes / grupper / stillinger / sessioner osv., Der giver klienten mulighed for at vise / skjule elementer og ruter baseret på dine tilladelser.

Når det kommer til at levere slutpunkter til opdatering af brugerpræferencer, tillader PATCH / mig at ændre disse intrinsiske værdier.

Giv pagination

Pagination er virkelig vigtig, fordi du ikke ønsker, at en enkel anmodning skal være utroligt dyr, hvis der er tusinder af rækker med resultater. Det virker indlysende, men mange forsømmer denne funktionalitet.

Der er flere måder at gøre dette på:

“Fra” -parameter

Det er nok den nemmeste at implementere, hvor API'et accepterer en fra forespørgselsstrengparameter og derefter returnerer et begrænset antal resultater fra denne forskydning (normalt 20 resultater).

Også bedst at tilvejebringe en grænseparameter, der har et hårdt maksimum, som f.eks. Twitter, med et maksimum på 1000 og standardgrænse på 200.

Token til næste side

Googles Places API returnerer et næste_side_token i dets svar, hvis der er mere information tilgængelig ud over de begrænsede 20 resultater pr. Side. Derefter accepterer den pagetoken som en parameter for en ny anmodning, som fortsætter med at returnere flere resultater med en ny næste_side_token, indtil den er opbrugt. Twitter gør en lignende ting i stedet ved hjælp af en parametre, der hedder next_cursor.

Svar

Brug konvolutter

”Jeg kan ikke lide at omslutte data. Det introducerer bare en anden nøgle til at navigere i et potentielt tæt datatræ. Meta-oplysninger skal gå i overskrifter. ”
”Et argument for indlejringsdata er at tilvejebringe to forskellige rodnøgler til at indikere succesen med svaret, * data * og * fejl *. Imidlertid delegerer jeg denne sondring til HTTP-statuskoder i tilfælde af fejl. "

Oprindeligt var jeg af den opfattelse, at det ikke er nødvendigt at omslutte data, og at HTTP leverede en passende "konvolut" i sig selv til at levere et svar. Efter at have læst gennem svar på Reddit kan der dog opstå forskellige sårbarheder og potentielle hacks, hvis du ikke omslutter JSON-matriser.

Du skal konvoluttere dine datasvar!

// DO: indhyllet
{
  data: [
    {...},
    {...},
    // ...
  ]
}

// IKKE-kuvert
[
  {...},
  {...},
  // ...
]
"Hvis du desuden gerne vil bruge et værktøj som normalizr til at analysere data fra svar fra klientsiden, fjerner du en konvolut behovet for konstant at udtrække dataene fra svarens nyttelast for at videregive, at de normaliseres."

Tværtimod, hvis du giver en ekstra nøgle til adgang til dine data, er det pålideligt at kontrollere, om der faktisk blev returneret noget, og hvis ikke, kan det henvise til en ikke-sammenstødende fejlnøgle adskilt fra svaret.

Det er også vigtigt at overveje, at sprog som JavaScript i modsætning til nogle vil evaluere tomme objekter som sande! Derfor er det vigtigt at ikke returnere et tomt objekt for fejl som en del af et svar i tilfælde af:

// kuvert, fejlekstraktion fra nyttelast
const {data, error} = nyttelast
// behandlingsfejl, hvis de findes
hvis (fejl) {kast ...}
// Ellers
const normalizedData = normalisere (data, skema)

JSON svar og anmodninger

”Alt skal serialiseres i JSON. Hvis du forventer JSON fra serveren, skal du være høflig og give serveren JSON. Konsistens!"

Naturligvis er "alt" en overdrivelse, som nogle kommentarer påpeger, men var beregnet til at henvise til ethvert enkelt, almindeligt objekt, der skulle serialiseres til processen med at konsumere og / eller vende tilbage fra API'en.

Det er vigtigt at definere dine medietyper gennem overskrifter på både svar og anmodninger om et RESTful API. Når du beskæftiger dig med JSON, skal du sikre dig, at du inkluderer en Content-Type:-applikation / json-header og henholdsvis for andre responstyper, det være sig CSV'er eller binære filer.

Returner det opdaterede objekt

Når du opdaterer en ressource gennem en PUT eller PATCH, er det god praksis at returnere den opdaterede ressource som svar på en vellykket POST-, PUT- eller PATCH-anmodning!

Brug 204 til sletning

Der har været tilfælde, hvor jeg ikke har haft noget at vende tilbage fra succesen med en handling (dvs. SLET), men jeg føler, at returnering af et tomt objekt på nogle sprog (som Python) kan vurderes som falsk og måske ikke er så indlysende til en menneskelig fejlsøgning af deres ansøgning.

Understøtt 204 - Ingen indholds svarstatuskode i tilfælde, hvor anmodningen var vellykket, men ikke har noget indhold, der skal returneres. Svarets konvolut kombineret med en 2XX HTTP-succeskode er nok til at indikere et vellykket svar uden vilkårlig "information".

SLET / v1 / indlæg /: id
// svar - 204
{
  "data": null
}

Brug HTTP-statuskoder og fejlsvar

Da vi bruger HTTP-metoder, bør vi bruge HTTP-statuskoder. Selvom en udfordring her er at vælge et distinkt udsnit af disse koder og derefter afhænge af svardata for at specificere eventuelle svarfejl. At holde et lille sæt koder hjælper dig med at konsumere og håndtere fejl konsekvent.

Jeg kan godt lide at bruge:

til datafejl

  • 400 for når de anmodede oplysninger er ufuldstændige eller misdannede.
  • 422 for når de anmodede oplysninger er i orden, men ugyldige.
  • 404 for når alt er i orden, men ressourcen findes ikke.
  • 409 for når der eksisterer en datakonflikt, selv med gyldige oplysninger.

til autentiske fejl

  • 401 for når der ikke findes et adgangstoken eller er ugyldigt.
  • 403 til, når et adgangstoken er gyldigt, men kræver flere privilegier.

for standardstatusser

  • 200 for når alt er i orden.
  • 204 for når alt er i orden, men der er intet indhold, der skal returneres.
  • 500 for når serveren kaster en fejl, helt uventet.

Endvidere er det også meget vigtigt at returnere svar efter disse fejl. Jeg vil ikke kun overveje præsentationen af ​​selve status, men også en grund bag den.

I tilfælde af at prøve at oprette en ny konto, kan du forestille dig, at vi giver en e-mail og adgangskode. Naturligvis vil vi gerne have, at vores klient-app forhindrer anmodninger med en ugyldig e-mail eller adgangskode, der er for kort, men udenfor har så stor adgang til API, som vi gør fra vores klient-app, når den er live.

  • Hvis e-mail-feltet mangler, skal du returnere en 400.
  • Hvis adgangskodefeltet er for kort, skal du returnere en 422.
  • Hvis e-mail-feltet ikke er en gyldig e-mail, skal du returnere en 422.
  • Hvis e-mailen allerede er taget, skal du returnere en 409.
"Det er meget bedre at specificere en mere specifik 4xx-seriekode end blot almindelig 400. Jeg forstår, at du kan lægge, hvad du vil, i svarorganet for at nedbryde fejlen, men koder er meget lettere at læse med et øjeblik." (Kilde)

Fra disse tilfælde returnerede to fejl 422s, uanset om deres grunde var forskellige. Derfor har vi brug for en fejlkode og måske endda en fejlbeskrivelse. Det er vigtigt at skelne mellem kode og beskrivelse, da jeg har til hensigt at have kode som en maskine forbrugsbar konstant og besked som en menneskelig forbrugsstoffer, der kan ændre sig.

I tilfælde af fejl per felt er tilstedeværelsen af ​​feltet som en nøgle i fejlen nok af en "kode" til at indikere, at det er et mål for en valideringsfejl.

Feltvalideringsfejl

For returnering af disse pr. Feltfejl kan det returneres som:

POST / v1 / register
// anmodning
{
  "email": "slut @@ user.comx"
  "password": "abc"
}
// svar - 422
{
  "fejl": {
    "kode": "FIELDS_VALIDATION_ERROR",
    "message": "Et eller flere felter rejste valideringsfejl."
    "felter": {
      "email": "Ugyldig e-mail-adresse.",
      "password": "Adgangskode er for kort."
    }
  }
}

Operationelle valideringsfejl

Og for returnering af operationelle valideringsfejl:

POST / v1 / register
// anmodning
{
  "e-mail": "end@user.com",
  "password": "password"
}
// svar - 409
{
  "fejl": {
    "kode": "EMAIL_ALREADY_EXISTS",
    "message": "En konto findes allerede med denne e-mail."
  }
}

Meddelelsen kan fungere som en menneskelig læseliv fejlmeddelelse, der kan læses for at hjælpe med at forstå anmodningen under udvikling, og i tilfælde af at en passende implementering af lokaliseringstrenge ikke kan bruges.

På denne måde følger din hentelogik op for ikke-200 fejl, og kan derefter lige op kontrollere fejlnøglen fra svaret og derefter sammenligne den med enhver yderligere logik i klientappen.

Godkendelse

Moderne statsløse, RESTful API'er implementerer godkendelse med tokens, der oftest leveres gennem autorisationshovedet (eller endda en access_token-forespørgselsparam).

Brug selvforlængende sessionstokens

Jeg troede oprindeligt, at udstedelse af JWT'er til regelmæssige API-anmodninger var en fantastisk måde at håndtere autentificering på - indtil jeg ønskede at ugyldige disse tokens.

I min sidste revision af dette indlæg (og detaljeret i et separat indlæg) tilbød jeg en måde, hvorpå JWT'er kunne genudstedes gennem en yderligere lagret klienthemmelighed “Refresh Token” (RT), som skulle udveksles med nye JWT'er. For at udløbe disse JWT'er indeholdt de imidlertid hver en henvisning til den udstedende RT, så hvis RT blev ugyldig / slettet, ville JWT også. Men denne mekanisme besejrer statsligheden af ​​JWT selv ...

Min løsning nu er simpelthen at bruge a / session-ressourceendepunktet til at udveksle login-legitimationsoplysninger til et enkelt unikt sessionstoken (ved hjælp af uuid4), som er hashet og gemt som en databaserække. Ligesom mange moderne apps, behøver token ikke at blive udstedt igen, medmindre der er en lang periode med inaktivitet (svarende til sessionoutbrud, men i størrelsesordenen uger). Efter førstegangsgodkendelse stødder enhver fremtidig anmodning tokenets levetid på en selvforlængende måde, så længe den ikke er udløbet.

Oprettelse af session - Log ind

En normal loginproces ser ud som:

  1. Modtag e-mail / adgangskodekombination med POST / sessioner, der behandler sessioner som bare en anden ressource.
  2. Kontroller e-mail / password-hash mod databasen.
  3. Opret en ny sessiondatabase-række, der indeholder en hashed uuid4 () som et token.
  4. Returner den ikke-hashede tokenstreng til klienten.

Fornyelse af session

I denne flow-tokens behøver ikke eksplicit fornyelse eller genudstedelse. Det skyldes, at API forlænger tokenens levetid, hvis det stadig er gyldigt for hver anmodning, hvilket sparer regelmæssige brugere fra nogensinde at have en session, der udløber for dem.

Hver gang et token modtages af API, dvs. gennem en autorisationshoved:

  1. Modtag token, dvs. fra autorisationshovedet.
  2. Sammenlign med tokenets hash, hvis der ikke er nogen matchende session række, skal du hæve en godkendelsesfejl.
  3. Kontroller egenskaben updated_at i sessionen, hvis sessionen betragtes som udløbet, hvis den er større end updated_at + session_life, slet sessionraden, hæv en godkendelsesfejl.
  4. Hvis det eksisterer og stadig er gyldigt fra opdateret_at-tid, skal du indstille det opdaterede_tid til nu () for at fornye token.

Session Management

Da alle sessioner spores som databaserækker, der er kortlagt til en bruger, kan en bruger se alle deres aktive sessioner, der ligner Facebooks kontosikkerhedssessioner. Du kan også vælge at inkludere alle tilknyttede metadata, du har valgt at samle, når du oprindeligt opretter en session, såsom browserens brugeragent, IP-adresse osv.

Hentning af alle dine sessioner er så simpelt som:

  1. FÅ / sessioner for at returnere alle sessioner, der er knyttet til din bruger via autorisationshovedet.

Session ophør - Log ud

Og fordi du har håndtag til dine sessioner, kan du afslutte dem for at ugyldiggøre uautoriseret eller uønsket adgang til din konto. Og at logge ud ville simpelthen være at afslutte klientens session og rense sessionen fra klienten.

  1. Modtag tokenet som en del af en DELETE / session / id-anmodning.
  2. Sammenlign med tokenets hash, slet den matchende session række.

Undgå regler om adgangskodekomposition

Efter at have foretaget en masse undersøgelse af kodeordregler, er jeg enig i, at adgangskodreglerne er bullshit og er en del af NISTs "don'ts", især i betragtning af at kodeordsammensætningsregler hjælper med at indsnævre gyldige adgangskoder baseret på deres gyldighedsregler.

Jeg har samlet nogle af de bedste punkter (fra ovenstående links) til adgangskodehåndtering:

  1. Håndhæv kun en minimum unicode-adgangskodelængde (min. 8-10).
  2. Kontroller mod almindelige adgangskoder (“password12345”)
  3. Kontroller for grundlæggende entropi (lad ikke "aaaaaaaaaaaa").
  4. Brug ikke kodeord, der opstiller regler (mindst en “! @ # $% &”).
  5. Brug ikke adgangskodetips (rim med "assword").
  6. Brug ikke videnbaseret autentificering ("sikkerhed" -spørgsmål).
  7. Undgå adgangskoder uden grund.
  8. Brug ikke SMS til tofaktorautentisering.
  9. Brug et kodeordssalt på 32 bit eller mere.

Disse "don'ts" bør gøre adgangskodevaluering meget lettere!

Meta

Brug et "Health-Check" slutpunkt

Gennem udvikling med AWS var det nødvendigt at give en måde at udsende et simpelt svar, der kan indikere, at API-forekomsten er i live og ikke behøver at blive genstartet. Det er også nyttigt til let at kontrollere, hvilken version af API der er på enhver maskine til enhver tid uden godkendelse.

GET / v1
// svar - 200
{
  "status": "kører",
  "version": "fdb1d5e"
}

Jeg leverer status og version (som henviser til git commit ref af API på det tidspunkt, det blev bygget). Det er også værd at nævne, at denne værdi ikke stammer fra en aktiv .git-repo, der er samlet med API-beholderen til EC2. I stedet læses den (og gemmes i hukommelsen) ved initialisering fra en version.txt-fil (som genereres fra buildprocessen), og standardindstilles til __UNKNOWN__ i tilfælde af en læsefejl, eller filen findes ikke.

Tak fordi du læste!

Hvis du nød min artikel og / eller fandt det nyttigt, ville jeg værdsætte, hvis du efterlader et klap eller to her på Medium og stjernemærker min artikel på GitHub ️.

Du er velkommen til at efterlade en kommentar nedenfor; lad os have en samtale!