Utarbeide APIer med skinner

I dag er det en vanlig praksis å stole tungt på APIer (applikasjonsprogrammeringsgrensesnitt). Ikke bare store tjenester som Facebook og Twitter bruker dem-APIer er svært populære på grunn av spredningen av klient-side rammer som React, Angular, og mange andre. Ruby on Rails følger denne trenden, og den nyeste versjonen presenterer en ny funksjon som lar deg lage API-eneste applikasjoner. 

I utgangspunktet ble denne funksjonaliteten pakket inn i en egen perle som heter rails-api, men siden utgivelsen av Rails 5 er den nå en del av rammekjernen. Denne funksjonen sammen med ActionCable var trolig den mest forventede, og så i dag skal vi diskutere det.

Denne artikkelen dekker hvordan du oppretter API-eneste Rails-applikasjoner, og forklarer hvordan du strukturerer ruter og kontroller, svarer med JSON-formatet, legger til serialisatorer og konfigurerer CORS (Cross-Origin Resource Sharing). Du vil også lære om noen alternativer for å sikre API og beskytte den mot misbruk.

Kilden til denne artikkelen er tilgjengelig på GitHub.

Opprette et API-eneste program

For å starte, kjør følgende kommando:

skinner nye RailsApiDemo --api

Det skal opprette en ny API-eneste Rails-applikasjon som heter RailsApiDemo. Ikke glem at støtten til --api alternativet ble bare lagt til i Rails 5, så sørg for at du har denne eller en nyere versjon installert.

Åpne Gemfile og merk at det er mye mindre enn vanlig: edelstener som kaffe-skinner, turbolinks, og Sass-skinnene er borte.

De config / application.rb filen inneholder en ny linje:

config.api_only = true

Det betyr at Rails skal laste inn et mindre sett med mellomvare: for eksempel er det ingen informasjonskapsler og økter som støtter. Videre, hvis du prøver å generere stillas, blir ikke visninger og eiendeler opprettet. Faktisk, hvis du sjekker visninger / oppsett katalog, vil du merke at application.html.erb filen mangler også.

En annen viktig forskjell er at ApplicationController arv fra ActionController :: API, ikke ActionController :: Base.

Det er ganske mye det - alt i alt, dette er et grunnleggende Rails program du har sett mange ganger. La oss nå legge til et par modeller slik at vi har noe å jobbe med:

skinner g modell Brukernavn: strengskinner g modell Posttittel: strenglegeme: tekstbruker: belong_to rails db: migrere

Ikke noe fancy skjer her: et innlegg med tittel, og en kropp tilhører en bruker.

Sørg for at de riktige foreningene er opprettet og også gi noen enkle valideringskontroller:

modeller / user.rb

 has_many: innlegg validerer: navn, tilstedeværelse: true

modeller / post.rb

 belongs_to: user validates: title, presence: true validates: body, presence: true

Strålende! Det neste trinnet er å laste inn et par utvalgsposter i de nyopprettede tabellene.

Laster demo data

Den enkleste måten å laste inn noen data på er å bruke seeds.rb fil inne i db katalogen. Imidlertid er jeg lat (som mange programmerere er) og vil ikke tenke på noen prøveinnhold. Derfor, hvorfor bruker vi ikke faker-perlen som kan produsere tilfeldige data av forskjellige slag: navn, e-post, hipster-ord, "lorem ipsum" -tekster og mye mer.

Gemfile

gruppe: utvikling gjør perlefakerens ende

Installer perlen:

bunt installasjon

Nå juster du seeds.rb:

db / seeds.rb

5. ganger bruker = Bruker.skap (navn: Faker :: Name.name) user.posts.create (tittel: Faker :: Book.title, body: Faker :: Lorem.sentence) avslutter

Til slutt, last opp dataene dine:

skinner db: frø

Å svare med JSON

Nå, selvfølgelig, trenger vi noen ruter og kontrollører til å lage vår API. Det er en vanlig praksis å nest API-ruter under api / sti. Dessuten gir utviklere vanligvis API-versjonen i banen, for eksempel api / v1 /. Senere, hvis noen bryte endringer må innføres, kan du ganske enkelt opprette et nytt navneområde (v2) og en egen kontroller.

Her ser du hvordan rutene dine kan se ut:

config / routes.rb

namespace 'api' gjør namespace 'v1' gjøre ressurser: innlegg ressurser: brukere slutten

Dette genererer ruter som:

api_v1_posts GET /api/v1/posts(.:format) api / v1 / innlegg # index POST /api/v1/posts(.:format) api / v1 / innlegg # opprett api_v1_post GET / api / v1 / innlegg /: id (.: format) api / v1 / innlegg # show

Du kan bruke en omfang metode i stedet for namespace, men da vil det som standard se etter UsersController og PostsController inne i kontrollere katalog, ikke inne i styringer / api / v1, Så vær forsiktig.

Opprett api mappe med den nestede katalogen v1 inne i kontrollere. Befolk det med dine kontrollere:

styringer / api / v1 / users_controller.rb

modul Api modul V1 klasse UsersController < ApplicationController end end end

styringer / api / v1 / posts_controller.rb

modul Api modul V1 klasse PostsController < ApplicationController end end end

Vær oppmerksom på at du ikke bare må nest kontrollenhetsfilen under api / v1 bane, men selve klassen må også være navngitt innenfor api og V1 moduler.

Det neste spørsmålet er hvordan du skal svare riktig med JSON-formaterte data? I denne artikkelen vil vi prøve disse løsningene: jBuilder og active_model_serializers edelstener. Så før du går videre til neste del, slipp dem inn i Gemfile:

Gemfile

perle 'jbuilder', '~> 2,5' perle 'active_model_serializers', '~> 0.10.0'

Kjør deretter:

bunt installasjon

Bruke jBuilder Gem

jBuilder er en populær perle opprettholdt av Rails teamet som gir et enkelt DSL (domenespesifikt språk) slik at du kan definere JSON strukturer i dine synspunkter.

Anta at vi ønsket å vise alle innleggene når en bruker treffer index handling:

styringer / api / v1 / posts_controller.rb

 def index @posts = Post.order ('created_at DESC') slutten

Alt du trenger å gjøre er å opprette visningen oppkalt etter den tilsvarende handlingen med .json.jbuilder forlengelse. Merk at visningen må plasseres under api / v1 sti også:

visninger / api / v1 / innlegg / index.json.jbuilder

json.array! @posts do | post | json.id post.id json.title post.title json.body post.body end

json.array! traverses the @posts matrise. json.id, json.title og json.body generere nøklene med de tilsvarende navnene som angir argumentene som verdiene. Hvis du navigerer til http: // localhost: 3000 / api / v1 / posts.json, ser du en utgang som ligner denne:

["id": 1, "tittel": "Tittel 1", "kropp": "Kropp 1", "id": 2, "tittel": "Tittel 2", "kropp" "]

Hva om vi ønsket å vise forfatteren for hvert innlegg også? Det er enkelt:

json.array! @posts do | post | json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end end

Utgangen vil endres til:

["id": 1, "tittel": "Tittel 1", "kropp": "Kropp 1", "bruker": "id": 1, "navn": "Brukernavn"]

Innholdet i .JBuilder filer er vanlig Ruby-kode, slik at du kan bruke alle de grunnleggende operasjonene som vanlig.

Vær oppmerksom på at jBuilder støtter delvise, akkurat som alle vanlige Rails-visninger, så du kan også si: 

json.partial! delvis: 'innlegg / post', samling: @posts, as:: post

og opprett deretter visninger / api / v1 / innlegg / _post.json.jbuilder fil med følgende innhold:

json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end

Så, som du ser, er jBuilder enkelt og praktisk. Men som et alternativ kan du holde fast med serialiseringene, så la oss diskutere dem i neste avsnitt.

Bruke Serializers

Rails_model_serializers perlen ble laget av et lag som i første omgang klarte skinnene-api. Som nevnt i dokumentasjonen, gir rails_model_serializers konvensjon over konfigurasjon til JSON-generasjonen. I utgangspunktet definerer du hvilke felt som skal brukes ved serialisering (det vil si JSON-generasjon).

Her er vår første serializer:

serial / post_serializer.rb

klasse PostSerializer < ActiveModel::Serializer attributes :id, :title, :body end

Her sier vi at alle disse feltene skal være til stede i den resulterende JSON. Nå er metoder som to_json og as_json Kald på et innlegg vil bruke denne konfigurasjonen og returnere det riktige innholdet.

For å se det i aksjon, endre index handling slik:

styringer / api / v1 / posts_controller.rb

def index @posts = Post.order ('created_at DESC') gjengi json: @posts end

as_json vil automatisk bli kalt på @posts gjenstand.

Hva med brukerne? Serializers lar deg indikere relasjoner, akkurat som modeller gjør. I tillegg kan serialisatorer være nestet:

serial / post_serializer.rb

klasse PostSerializer < ActiveModel::Serializer attributes :id, :title, :body belongs_to :user class UserSerializer < ActiveModel::Serializer attributes :id, :name end end

Nå når du serialiserer innlegget, vil det automatisk inneholde den nestede bruker nøkkel med id og navn. Hvis du senere oppretter en separat serialiser for brukeren med : id attributt ekskludert:

serial / post_serializer.rb

klasse UserSerializer < ActiveModel::Serializer attributes :name end

deretter @ user.as_json vil ikke returnere brukerens ID. Fortsatt, @ post.as_json Vil returnere både brukerens navn og ID, så husk det.

Sikre API

I mange tilfeller vil vi ikke at noen bare skal utføre noen handlinger ved hjelp av API. Så la oss presentere en enkel sikkerhetskontroll og tvinge brukerne våre til å sende sine tokens når de oppretter og sletter innlegg.

Tollet vil ha en ubegrenset levetid og bli opprettet ved brukernes registrering. Først av alt, legg til en ny pollett kolonne til brukere bord:

skinner g migrasjon add_token_to_users token: string: index

Denne indeksen skal garantere unikhet, da det ikke kan være to brukere med samme tegn:

db / migrerer / xyz_add_token_to_users.rb

add_index: brukere,: token, unikt: true

Bruk overføringen:

skinner db: migrere

Legg nå til before_save Ring tilbake:

modeller / user.rb

before_create -> self.token = generate_token

De generate_token privat metode vil skape et token i en endeløs syklus og kontrollere om det er unikt eller ikke. Så snart et unikt token er funnet, returner det:

modeller / user.rb

privat def generate_token loop gjør token = SecureRandom.hex retur token med mindre User.exists? (token: token) endeenden

Du kan bruke en annen algoritme til å generere token, for eksempel basert på MD5-hash av brukerens navn og litt salt.

bruker registrering

Selvfølgelig må vi også tillate brukere å registrere seg, fordi ellers vil de ikke kunne skaffe seg token. Jeg vil ikke presentere noen HTML-visninger i vår søknad, så la oss legge til en ny API-metode:

styringer / api / v1 / users_controller.rb

def opprett @user = User.new (user_params) hvis @ user.save gjengestatus:: opprettet annet gjengi json: @ user.errors, status:: unprocessable_entity slutten privat def user_params params.require (: user) .permit (: navn) slutt

Det er en god ide å returnere meningsfylte HTTP-statuskoder slik at utviklere forstår nøyaktig hva som skjer. Nå kan du enten gi en ny serializer til brukerne eller holde fast med en .json.jbuilder fil. Jeg foretrekker sistnevnte variant (derfor passerer jeg ikke : json alternativ til gjengi metode), men du er fri til å velge noen av dem. Merk imidlertid at token må ikke være alltid serialisert, for eksempel når du returnerer en liste over alle brukere - det bør holdes trygt!

visninger / api / v1 / brukere / create.json.jbuilder

json.id @ user.id json.name @ user.name json.token @ user.token

Det neste trinnet er å teste om alt fungerer som det skal. Du kan enten bruke curl kommandoen eller skriv litt Ruby-kode. Siden denne artikkelen handler om Ruby, går jeg med kodingsalternativet.

Testing Brukerens Registrering

For å utføre en HTTP-forespørsel, vil vi bruke Faraday-perlen, som gir et felles grensesnitt over mange adaptere (standard er Net :: HTTP). Opprett en egen Ruby-fil, inkludert Faraday, og sett opp klienten:

api_client.rb

krever faraday klient = Faraday.new (url: 'http: // localhost: 3000') do | config | config.adapter Faraday.default_adapter end response = client.post do | req | req.url '/ api / v1 / users' req.headers ['Content-Type'] = 'application / json' req.body = '"bruker": "navn": "test bruker"

Alle disse alternativene er ganske selvforklarende: Vi velger standardadapteren, sett forespørselsadressen til http: // localhost: 300 / api / v1 / users, endre innholdstypen til application / json, og gi kroppen vår forespørsel.

Serverens svar skal inneholde JSON, så å analysere det jeg bruker Oj-perlen:

api_client.rb

krever 'oj' # klient her ... setter Oj.load (response.body) setter response.status

Bortsett fra det analyserte svaret, viser jeg også statuskoden for feilsøkingsformål.

Nå kan du bare kjøre dette skriptet:

rubin api_client.rb

og lagre det mottatte token et sted, vi bruker det i neste avsnitt.

Godkjenning med Token

For å håndheve token-godkjenningen, authenticate_or_request_with_http_token Metoden kan brukes. Det er en del av ActionController :: HttpAuthentication :: Token :: ControllerMethods-modulen, så ikke glem å inkludere den:

styringer / api / v1 / posts_controller.rb 

klasse PostsController < ApplicationController include ActionController::HttpAuthentication::Token::ControllerMethods #… end

Legg til en ny before_action og den tilsvarende metoden:

styringer / api / v1 / posts_controller.rb 

before_action: autentiser bare: [: create,: destroy] # ... private # ... def autentisere authenticate_or_request_with_http_token do | token, alternativer | @user = User.find_by (token: token) slutten

Nå, hvis token ikke er angitt, eller hvis en bruker med et slikt tegn ikke kan bli funnet, vil en 401-feil returneres, stoppe handlingen fra å utføre.

Vær oppmerksom på at kommunikasjonen mellom klienten og serveren må gjøres over HTTPS, fordi ellers kan tokens enkelt spoofed. Selvfølgelig er den tilveiebragte løsningen ikke ideell, og i mange tilfeller er det å foretrekke å benytte OAuth 2-protokollen for autentisering. Det er minst to perler som i stor grad forenkler prosessen med å støtte denne funksjonen: Dørvakt og oPRO.

Opprette et innlegg

For å se vår godkjenning i handling legger du til skape handling til PostsController:

styringer / api / v1 / posts_controller.rb 

def opprett @post = @ user.posts.new (post_params) hvis @ post.save gjengi json: @post, status:: opprettet annet gjengi json: @ post.errors, status:: unprocessable_entity slutten

Vi benytter serializer her for å vise riktig JSON. De @bruker var allerede satt inne i before_action.

Test nå alt ut med denne enkle koden:

api_client.rb

klient = Faraday.new (url: 'http: // localhost: 3000') do | config | config.adapter Faraday.default_adapter config.token_auth ('127a74dbec6f156401b236d6cb32db0d') sluttrespons = client.post do | req | req.url '/ api / v1 / posts' req.headers ['Content-Type'] = 'søknad / json' req.body = '"post": "title": "Title", "body" "Tekst" 'ende

Erstatt argumentet som er overført til token_auth med token mottatt ved registrering, og kjøre skriptet.

rubin api_client.rb

Sletter et innlegg

Sletting av et innlegg gjøres på samme måte. Legg til ødelegge handling:

styringer / api / v1 / posts_controller.rb 

def ødelegge @post = @ user.posts.find_by (params [: id]) hvis @post @ post.destroy annet gjengi json: post: "ikke funnet", status:: ikke funnet slutten

Vi tillater bare brukere å ødelegge innleggene de egentlig eier. Hvis innlegget blir fjernet, vil 204 statuskoden (ingen innhold) returneres. Alternativt kan du svare med postens ID som ble slettet, da det fortsatt vil være tilgjengelig fra minnet.

Her er koden for å teste denne nye funksjonen:

api_client.rb

respons = client.delete do | req | req.url '/ api / v1 / posts / 6' req.headers ['Content-Type'] = 'søknad / json' ende

Erstatt postens ID med et nummer som fungerer for deg.

Sette opp CORS

Hvis du vil aktivere andre webtjenester for å få tilgang til API-en din (fra klientsiden), bør CORS (Cross-Origin Resource Sharing) settes riktig opp. I utgangspunktet lar CORS webapplikasjoner sende AJAX-forespørsler til tredjepartstjenestene. Heldigvis er det en perle som heter rack-cors som gjør at vi enkelt kan sette alt opp. Legg det inn i Gemfile:

Gemfile

perle 'rack-cors'

Installer den:

bunt installasjon

Og så gi konfigurasjonen inne i konfig / initializers / cors.rb fil. Faktisk er denne filen allerede opprettet for deg og inneholder et brukseksempel. Du kan også finne noen ganske detaljert dokumentasjon på perlens side.

Følgende konfigurasjon vil for eksempel tillate at noen får tilgang til API-en din ved hjelp av en hvilken som helst metode:

konfig / initializers / cors.rb

Rails.application.config.middleware.insert_before 0, Rack :: Kors gjør tillat opprinnelse '*' ressurs '/ api / *', overskrifter:: noen metoder: [: get,: post,: put,: patch, : Slett,: Valg,: Hodet], slutt

Forhindre misbruk

Det siste jeg skal nevne i denne veiledningen, er hvordan du beskytter API-en din mot misbruk og nektelse av tjenestenangrep. Det er en fin perle kalt rack-attack (opprettet av folk fra Kickstarter) som lar deg svarteliste eller hviteliste klienter, forhindre oversvømmelse av en server med forespørsler og mer.

Slip perlen inn i Gemfile:

Gemfile

perle 'rack-attack'

Installer den:

bunt installasjon

Og deretter gi konfigurasjon inne i rack_attack.rb initialiseringsfilen. Gemens dokumentasjon viser alle tilgjengelige alternativer og foreslår noen brukstilfeller. Her er sample-konfigurasjonen som begrenser alle, unntatt deg fra å få tilgang til tjenesten, og begrenser det maksimale antall forespørsler til 5 per sekund:

konfig / initializers / rack_attack.rb

class rack :: Attack safelist ('la fra localhost') do | req | # Forespørsler er tillatt hvis returverdien er sannferdig '127.0.0.1' == req.ip || ':: 1' == req.ip ende gasspjeld ('req / ip',: grense => 5,: periode => 1.sekond) do | req | req.ip slutten

En annen ting som må gjøres er blant annet RackAttack som mellomvare:

config / application.rb

config.middleware.use Rack :: Attack

Konklusjon

Vi har kommet til slutten av denne artikkelen. Forhåpentligvis, i dag føler du deg mer trygg på å lage APIer med Rails! Vær oppmerksom på at dette ikke er det eneste tilgjengelige alternativet - en annen populær løsning som eksisterte lenge, er Grape-rammen, så du kan også være interessert i å sjekke det ut også.

Ikke nøl med å legge inn dine spørsmål hvis noe virket uklart for deg. Jeg takker for at du bodde hos meg og lykkelig koding!