REST

REST – REpresentational State Transfer – er en programvarearkitektur som sikrer løse koblinger mellom to tjenester i en integrasjon. I dag anvendes arkitekturen først og fremst for å utvikle såkalte REST APIer. Et REST API er en skalerbar webtjeneste hvor en klient kan lese og manipulere ressurser på en tjener, som regel med HTTP som kommunikasjonsprotokoll. REST ble først introdusert for to tiår siden i en doktorgradsavhandling av Roy Fielding, som også er en av arkitektene bak HTTP.

Følgende figur illustrer den grunnleggende virkemåten til et REST API:

REST API - Grunnleggende virkemåte

Figur 1: Den grunnleggende virkemåten til et REST API

En klient sender en forespørsel om en ressurs til APIet. Når kommunikasjonen går over HTTP vil forespørselen inneholde en URL som identifiserer ressursen, nødvendige tilstandsdata og ønsket operasjon (f.eks. GET for å hente eller DELETE for å slette). Tjeneren prosesserer forespørselen og sender en respons tilbake til klienten. Responsen inneholder en representasjon av den etterspurte ressursen, vanligvis i formatet JSON eller XML.

Prinsipper

Når man skal utvikle et REST API må man følge et sett med prinsipper. Fielding kalte disse prinsippene for arkitektoniske begrensninger, fordi de begrenser hvordan APIet kan prosessere og respondere på forespørsler fra en klient. Følger man disse prinsippene oppnår man i teorien en rekke fordeler knyttet til blant annet ytelse, skalerbarhet, vedlikeholdbarhet, portabilitet og pålitelighet. APIet er da "RESTful". Vi skal nå gå gjennom de viktigste prinsippene.

Klient/tjener-arkitektur

Klient/tjener er en arkitektur som separerer programvaren som etterspør en ressurs eller tjeneste (klienten) fra programvaren som prosesserer og responderer på forespørselen (tjeneren). All kommunikasjon mellom klienten og tjeneren foregår over en veldefinert protokoll. Som oftest brukes HTTP som kommunikasjonsprotokoll, men det er viktig å merke seg at andre protokoller kan benyttes. Klientens ansvarsområde er først og fremst å tilby et brukergrensesnitt. Forretningslogikk, databasetilgang og sikkerhet er tjenerens ansvarsområde. Denne separasjonen gir oss flere fordeler. Ulike klienter kan benytte samme tjener, det er enklere å skalere for økt trafikk ved å øke antall tjenere, og programvarekomponentene på klienten og tjeneren kan utvikles og vedlikeholdes uavhengig av hverandre – de er løst koblet.

Klient/tjener-arkitektur

Figur 2: Klient/tjener-arkitektur

Tilstandsløs kommunikasjon

Når en klient sender en forespørsel til tjeneren sender den med all informasjon som trengs for å behandle forespørselen. Dette gjør klienten hver gang en ny forespørsel sendes. Det er klienten som er ansvarlig for å til enhver tid ha oversikt over tilstanden i sesjonen. Det er derfor ikke nødvendig å mellomlagre tilstand på tjeneren. Siden tjeneren ikke kjenner klientens kontekst utover det som mottas i forespørselen sier vi at protokollen er tilstandsløs.

Denne begrensningen er viktig for å sikre en løs kobling mellom klienten og tjeneren. Ulempen er at klientens tilstand må sendes på nytt for hver forespørsel. Vi må altså sende mer data enn hvis tjeneren hadde mellomlagret tilstanden, noe som kan påvirke ytelsen i nettverket.

Lagdeling

En klient vet ikke om den kommuniserer direkte med tjeneren eller via et mellomledd. Vi kan ha flere lag mellom klienten og den endelige tjeneren. I ett lag kan vi tilby belastningsfordeling, caching i et annet. I praksis betyr det at vi ofte er innom flere tjenere før vi får svar. Lagdeling gjør det enklere å skalere opp tjenestene våre, men det introduserer også økt kompleksitet i systemet. Har vi for mange mellomledd kan det dessuten oppstå forsinkelser, som igjen gir en dårligere brukeropplevelse.

I figur 3 har vi introdusert en ny tjener som fungerer som belastningsfordeler (LB, "load balancer") for APIet vårt. Prinsippet om lagdeling gjør dette mulig.

Lagdeling

Figur 3: Belastningsfordeling (LB) introduserer et nytt lag i modellen

Caching

En cache er et midlertidig lager med data. Hvis en tjener mottar samme forespørsel mange ganger er det ofte en fordel å lagre en kopi av dataene som etterspørres i dette mellomlageret. Dette er relevant for data som ikke endres ofte, f.eks. bilder og statiske dokumenter. Klienten vil da få respons direkte fra mellomlageret, som er mer effektivt enn å måtte prosessere forespørselen gjennom alle lagene i systemet.

REST forteller oss at enhver respons må defineres som enten "cacheable" eller "non-cacheable". De faktiske mekanismene for å oppnå dette avhenger av kommunikasjonsprotokollen. I HTTP kan man angi når en respons foreldes ved å legge til "Expires" og en dato i headeren til responsen. Alternativt kan man benytte "Cache-Control" og "max-age" for å angi i sekunder hvor lenge en respons er gyldig.

I figuren under introduserer vi en egen tjener som fungerer som et mellomlager. Prinsippet om lagdeling er også gjeldende her.

Caching

Figur 4: En cache kan effektivisere systemet

Uniformt grensesnitt

En ressurs må kunne identifiseres gjennom en unik URI (Uniform Resource Identifier), og det skal kun være mulig å manipulere ressursen ved å bruke metodene som tilbys i den underliggende kommunikasjonsprotokollen. Det som overføres må dessuten være en representasjon av ressursen i et velkjent format. Med HTTP er URIen en URL, og det er HTTP-verbet som bestemmer operasjonen man ønsker å utføre. For å hente en ressurs bruker man GET, mens oppretting, endring og sletting gjøres med henholdsvis POST, PUT/PATCH og DELETE. Ressursens format angis med en bestemt media type (vanligvis JSON eller XML).

Forespørselen

En forespørsel går fra en klient til en tjener og vil alltid inneholde et endepunkt, en metode og en header. Noen ganger sender vi også med tilstandsdata.

Endepunkt

Når en klient sender en forespørsel må den oppgi et endepunkt. Endepunktet er en URL bestående av en rot og en sti. Rota er startpunktet til APIet, mens stien identifiserer ressursen vi er ute etter. Man kan også legge til informasjon på slutten av URLen gjennom en query-streng med nøkkel/verdi-par, også kalt query-parametre.

Eksempel med GitLabs REST API:

Metode

Man angir hva slags metode man ønsker med et HTTP-verb. Man kan velge mellom følgende verb:

Metodene GET, PUT og DELETE er idempotente. Det vil si at de skal produsere det samme resultatet uansett hvor mange ganger de kjører. Med POST står man friere, og kan f.eks. gi tilbakemelding om at en gitt ressurs allerede eksisterer. Det er også verdt å merke seg at selv om både PUT og PATCH oppdaterer en ressurs, så er måten de gjør det på forskjellig. Med PATCH kan vi sende med kun den delen av ressursen som skal oppdateres. Bruker vi PUT må vi sende med informasjon om hele ressursen for å sikre at vi får likt resultat hver gang.

Ressursen vil alltid identifiseres gjennom samme endepunkt, mens metoden bestemmer om man ønsker å opprette, hente, endre eller slette ressursen. Dette gir oss grunnleggende CRUD-funksjonalitet: Create-Read-Update-Delete.

Blacboard Learn sitt REST API for kunngjøringer illustrerer konseptet på en fin måte:

MetodeEndepunktBeskrivelse
GET/learn/api/public/v1/announcementsHent alle kunngjøringer
GET/learn/api/public/v1/announcements/{announcementId}Hent en gitt kunngjøring
POST/learn/api/public/v1/announcementsLag en ny kunngjøring
DELETE/learn/api/public/v1/announcements/{announcementId}Slett en gitt kunngjøring
PATCH/learn/api/public/v1/announcements/{announcementId}Oppdater en gitt kunngjøring

Alle forespørsler har en header. Headeren inneholder tilleggsinformasjon eller metadata i form av nøkkel/verdi-par. En forespørsel kan f.eks. angi at forventet format på innholdet er HTML ved å oppgi Accept: text/html.

Tilstandsdata

Når vi lager eller oppdaterer en ressurs må vi som regel sende med data fra klienten. Vi kan velge å sende disse som query-parametre, men det er ikke alltid hensiktsmessig. Når vi skal sende mye informasjon bør vi bruke forespørselens kropp (body), som er bedre egnet for å overføre JSON eller XML. Det er også verdt å merke seg at vi aldri bør sende sensitive data som en del av en URL, selv om vi benytter HTTPS.

Responsen

Responsen har også en header. Ønsker man f.eks. å fortelle klienten at innholdet i responsen er JSON-formatert kan man oppgi Content-Type: application/json.

Når en tjener svarer sender den med en statuskode i headeren:

Det er vanlig å sende data tilbake til klienten enten som HTML, XML eller JSON.

Tenk deg at vi har et REST API som tilbyr metadata om bøker. En klient sender følgende forespørsel for å få informasjon om en bestemt bok:

Tjeneren vil behandle forespørselen, typisk ved å hente ut data om den etterspurte boka fra et mellomlager eller en database, og returnere følgende:

Versjonering

Hvis vi som utviklere skal gjøre større endringer i et REST API må vi passe på at vi ikke endrer grensesnittet. I prinsippet skal URLene vi tilbyr aldri endres, da dette bryter kontrakten som APIet har med klientene sine. Noen ganger må vi likevel gjøre korreksjoner og forbedringer i endepunktene. Vi løser denne utfordringen med versjonering. For mer omfattende endringer angir vi versjon ved å legge til et versjonsnummer i selve URLen. I eksemplene over kan vi se at GitLab er på versjon 4 av sitt API, mens Blackboard Learn er på versjon 1:

Denne strategien passer best når man skal versjonere hele APIet samlet. Hvis vi vil versjonere for et enkelt endepunkt eller på ressurs-nivå kan vi benytte Accept-headeren.

Nå som vi har kunnskap om prinsippene bak REST kan vi bygge et enkelt REST API. De fleste moderne programmeringsspråk tilbyr mekanismer for å gjøre dette. Siden vi bruker Javascript i dette kurset vil vi implementere eksemplet vårt basert på Node.js og webtjeneren Express.

REST API med Node og Express

Vi skal utvikle et enkelt REST API for en Todo-app. APIet skal tilby endepunkter for å hente ut, legge til og slette oppgaver:

MetodeEndepunkt (sti)Beskrivelse
GET/api/v1/tasksHent alle oppgaver
GET/api/v1/tasks/:idHent en gitt oppgave
POST/api/v1/tasksLag en ny oppgave
DELETE/api/v1/tasks/:idSlett en gitt oppgave

For å komme i mål må vi gjennom følgende steg:

  1. Opprett et nytt prosjekt

  2. Installer avhengigheter

  3. Opprett en dummy-database med testdata

  4. Start en webtjener

  5. Legg til endepunkter og test

Før vi begynner

Node.js og NPM

Før vi begynner må vi installere Node.js og NPM. Node.js er et runtime-system for å kjøre JavaScript-kode utenfor nettleseren, mens NPM er en pakkebehandler. Sistnevnte gjør det enklere å installere biblioteker vi kan benytte oss av når vi programmerer. Verktøyene lastes ned samlet fra nodejs.org.

Postman

Vi bruker Postman for manuell testing av APIet vårt. Verktøyet tilbyr et grafisk brukergrensesnitt for å lage og sende forespørsler. Last ned Postman fra postman.com/downloads.

Opprett et nytt prosjekt

Det første vi må gjøre er å opprette et nytt prosjekt. Lag en ny katalog og kall den todo-api:

Initialiser prosjektet ved å generere filen "package.json" med npm:

Filen "package.json" inneholder metadata som er relevant for prosjektet: prosjektnavn, versjon, kjøreskript, avhengigheter med mer. Når vi bruker flagget yes vil npm fylles med standardverdier.

Installer avhengigheter

Neste steg er å installere og konfigurere bibliotekene vi trenger.

Express

Express.js er et rammeverk som gjør det enkelt å sette opp en webtjener og legge til endepunkter for et REST API:

Express er nå lagt til som en modul i prosjektet under node_modules-katalogen. Flagget save registrerer modulen som en avhengighet i package.json-fila.

Babel og nodemon

Vi må også installere Babel, fordi vi skal bruke nyere JS-synktaks:

Babel er avhengig av en c-kompilator for visse funksjoner, så hvis du bruker macOS og får feilmeldingen "gyp: No Xcode or CLT version detected!" må du først følge instruksjonene på denne siden. Instruksjonene er også relevante hvis du får en liknende feilmelding i Windows.

Opprett så fila ".babelrc". Den må ligge i rotkatalogen for prosjektet, altså i "todo-api". Fila skal ha følgende innhold:

Fila forteller Babel at vi bruker nyere syntaks i form av ES6-kode (ECMAScript 2015). ES6 gir oss blant annet klasser og arrow-funksjoner. Node.js har ikke full støtte for denne syntaksen, så vi må transpilere ES6-koden vår til kompatibel form, altså ES5. En måte å gjøre det på er å bruke kommandoen node_modules/.bin/babel-node. Vi forenkler kjøringen av denne kommandoen ved å installere et verktøy kalt nodemon.

Så legger vi til et skript i package.json-fila kalt "start":

Skriptet transpilerer kildekoden i "app.js" til kompatibel form. Samtidig starter det en tjener som lytter etter endringer i koden og fanger opp disse automatisk, noe som forenkler utviklingen av appen vår. Skriptet kan kjøres med følgende kommando:

Kjører du skriptet nå vil det feile fordi filen "app.js" ikke eksisterer. Vi skal straks lage denne, men først må vi opprette en dummy-database og legge til litt testdata.

Opprett en dummy-database med testdata

Hver oppgave i todo-listen vår har en unik id, en tittel og et flagg som sier om oppgaven er ferdig eller ikke. Vi kunne tenkt oss at vi persisterte oppgavene våre i en reell database, men for enkelhets skyld lagrer vi oppgavene i en liste-variabel i minnet. Opprett filen "data.js" og legg inn følgende:

Start en webtjener

Vi kan nå utvikle selve appen. Vi starter med å initalisere en ny Express-webtjener som lytter på port 3000. Lag filen "app.js" og skriv inn følgende kode:

Vi bruker import for å hente inn ressurser fra andre moduler. Vi kan importere listen tasks fordi den ble tilgjengeliggjort fra "data.js" med export default tasks. Nå har vi tilgang til både express og testdataene våre. Linje 4 initialiserer appen vår, mens linje 5 gjør det mulig å tolke json i body-elementet til en forespørsel. Linje 7-10 forteller oss at webtjeneren skal lytte på port 3000. Startpunktet for vår lokale webtjener blir da http://localhost:3000.

Start webtjeneren fra terminalen med følgende kommando:

Neste steg er å legge inn endepunkter for å hente ut, opprette og slette oppgaver. Vi skal også teste endepunktene manuelt med Postman. Merk at det ikke er nødvendig å restarte APIet underveis fordi alle endringer i koden fanges opp automatisk med nodemon.

Legg til endepunkter

Hent ut alle oppgaver

Utvid "app.js" med følgende kode:

Her definerer vi et endepunkt som henter ut alle opppgavene fra listen tasks. Vi når endepunktet med ruten /api/v1/tasks. Responsen vil inneholde oppgavene i json-format.

Test med Postman

Vi kan nå teste det første endepunktet vårt med Postman:

  1. Velg "GET" som metode.

  2. Legg til URL-en localhost:3000/api/v1/tasks

  3. Klikk på "Send"-knappen. Postman sender nå en forespørsel til APIet vårt. Responsen vil vises i den nederste delen av vinduet.

Uthenting av oppgaver med Postman

Figur 5: Uthenting av alle oppgaver med Postman

Hent ut en bestemt oppgave

For å hente ut en bestemt oppgave må vi utvide "app.js" med følgende kodesnutt:

Igjen oppretter vi et get-endepunkt, men ruten inneholder nå også variabelen :id. Denne hentes ut på linje 2. På linje 3 bruker vi Array.find()for å søke blant oppgavene våre. Får vi treff responderer vi med opppgaven i json-format (linje 6), hvis oppgaven ikke finnes returnerer vi en melding med statuskode 404 Not Found (linje 8).

Test med Postman

Velg GET som metode og legg inn localhost:3000/api/v1/tasks/2 som URL. APIet vil returnere oppgaven i json-format som vist i figuren under.

Uthenting av en bestemt oppgave med Postman

Figur 6: Uthenting av en bestemt oppgave med Postman

Lag en ny oppgave

For å opprette en oppgave er det naturlig å bruke HTTP-metoden POST:

Vi forventer at oppgaven er json-formatert og ligger i forespørselens body-element. Den nye oppgaven hentes ut direkte på linje 2. På linje 4 - 10 kjøres enkle valideringsrutiner; vi sjekker at vi har mottatt nødvendige data og om forespørselen eksisterer fra før av. Hvis valideringen feiler svarer vi med en informativ melding og statuskoden 400 Bad Request (linje 11). Hvis alt er som det skal oppdaterer vi tasks med den nye oppgaven og sender en respons til klienten (linje 13-16).

Test med Postman

I Postman må vi gjøre følgende:

  1. Velg POST som metode.

  2. Legg til URL-en localhost:3000/api/v1/tasks

  3. Klikk på Body-fanen, velg "raw" og velg "JSON" som innholdstype fra nedtrekksmenyen.

  4. Skriv inn følgende i input-feltet for Body og klikk på "Send"-knappen:

Hvis alt går bra vil APIet respondere med statuskoden 201 Created og stien til den nye ressursen i headerfeltet Location .

Opprette en ny oppgave med Postman

Figur 7: Ny oppgave med Postman

Slett en oppgave

Til slutt gjør vi det mulig å slette en gitt oppgave. Vi bruker HTTP-metoden DELETE til dette:

Igjen bruker vi variabelen :id for å identifisere oppgaven vi er ute etter. På linje 3 bruker vi Array.findIndex()som returnerer posisjonen til oppgaven eller -1 hvis den ikke finnes. Hvis oppgaven finnes fjerner vi den fra listen med Array.splice()og sender en respons tilbake til klienten (linje 4-6). Hvis den ikke finnes svarer vi med en informativ feilmelding og statuskoden 404 Not Found (linje 8).

Test med Postman

Velg DELETE som metode og legg inn localhost:3000/api/v1/tasks/1 som URL. Dette vil slette den første oppgaven i lista. Hvis alt går som det skal vil APIet respondere med statuskode 200. For å illustrere at oppgaven med id "1" er fjernet fra listen sender vi også med den oppdaterte lista, selv om dette i praksis bryter med regelen om at DELETE skal være idempotent.

Slette en oppgave med Postman

Figur 8: Slette en oppgave med Postman

Kildekode

Fullstendig kildekode kan lastes ned fra https://gitlab.com/ntnu-dcst2002/todo-api.