Konstruáljunk Web API-t!

Amikor a legtöbben meghallják azt a rövidítést, hogy API, rendkívül különféle dolgokra asszociálnak. Van akinek a Java Persistence API jut eszébe, van akinek a Facebook API, míg másoknak valami teljesen más…
Na de mit is jelent maga a rövidítés?
Az API, az alkalmazás-programozási interfész rövidítése hivatott lenni, de aki eddig nem tudta mi az, azt most se hoztam közelebb a valósághoz. Ha az API kifejezést használjuk, akkor egy program azon funkcióit vagy szolgáltatásait soroljuk ide, amiket kívülről meg tudunk hívni és mindennek használatáról jó esetben dokumentáció is született. A lényeg, hogy nekünk nem kell tudnunk mi is történik a mélyben, mi csak használjuk a program nyújtotta szolgáltatásokat. Az előző példákat használva, a JPA (Java Persistence API) során nem kell tudnunk, hogy is kapcsolódik az adatbázishoz, hogy is menti le az entitásokat, stb. nekünk csak használni kell azt. Ugyanez van a Facebook API-val is. Nem tudjuk miben van tárolva az adat és nem tudjuk hol is van az, mi csak meghívunk egy webes URL-t adott paraméterrel és payloaddal és bumm, magic.
Mindezt használhatjuk egyazon programnyelvben is, amikor is létrehozunk egy csomagot, amit mások tudnak használni egy publikus API-n keresztül, de lehetséges mindez webes API-kon át.
Láthatjuk, hogy az egész kissé képlékeny, ezért nézzünk egy-egy példát.
Tegyük fel, hogy létrehoztunk egy tuti form validáló lib-et, amit boldogan használunk a saját kis applikációnkban. Egy idő után úgy döntünk, hogy jófejek leszünk és mindezt megosztjuk az open-source közösséggel. Felkerül packagist-re (vagy bárhova), a kód github-ra is. A githubos readme-ben szépen le van írva, hogy is tudják az emberek használni azt, tehát dokumentáltuk az API-t, amin keresztül el tudják érni a csomag nyújtotta szolgáltatásokat. Aki csak lehúzza azt magának függőségként, annak nem kell ismernie, hogy is működik, csak annyit kell tudnia, hogy mely publikus metódusokon keresztül éri el és azokat hogy kell használni. Emlékszünk még a facade patternre, ugye?
A másik opció, hogy készítettünk egy oldalt, ahol az emberek különféle csoportokat tudnak létrehozni, azokon belül pedig mindenféle szavazásokat csinálni. Így már legalább 100 scrum team létrehozott nálunk egy Slacker of the day szavazást, ahova gyűlik az infó. Na most ez eddig tök jó, viszont az oldalunk nagyon nem reszponzív, de megkeresnek minket, hogy csinálnának hozzá egy mobilapplikációt, csak nincs webes API hozzáírva. Na most backenddel jobban vagyunk, mint a design-al, így ismét csúcsra járatjuk a jófejségünket és írunk hozzá egy ún. REST (erről majd később) API-t, amin át a mobilapplikációkkal (vagy éppen egy másik webalkalmazással) is el tudják érni az oldalunk nyújtotta szolgáltatásokat, tehát tudnak majd szavazni, lekérdezni, stb.
Azért, hogy kicsit visszazuhanjunk az egyszerű valóságba, akkor jöjjön az, hogy aki bármit piszkált PHP-vel SQL adatbázisokban, az a PHP egyik MySQL kapcsolatokért felelős API-ját használta (mysql, mysqli, PDO).
Na de a mai témánk most a webes API lesz, ezért beszéljünk erről egy kicsit. Jelen esetben az API dokumentációja azt írja le, hogy is tudjuk piszkálni a szolgáltatásokat: milyen URI-n át, milyen HTTP metódusokkal, mi legyen a query stringben, milyen header-ökkel, mit küldjünk a request body-ban és milyen formában kapunk majd választ.
A web API-knak alapvetően két nagy formáját különböztetjük megy, az RPC (remote procedure call) és REST (representational state transfer) API-t.
RPC
RPC esetében az esetek többségében egyetlen URI-t hivogatunk, mégpedig POST metódussal. Az, hogy mi is a cél, azt a küldött payload határozza meg. Ez általában egy struktúrált kérés, amiben benne lesz az adott művelet neve, valamint a paraméterek. Itt két módszert különböztetünk meg, XML-RPC-t és SOAP-ot. Ez utóbbiról regényeket lehetne írni, így arról most nem írnék, de ha lesz érdeklődés, akkor szívesen taglalom majd egy bejegyzés során. De nézzünk egy példát:
1
2
3
4
5
6
7
8
9
10
11
12
POST /xml-rpc HTTP/1.1
Content-Type: text/xml
<?xml version="1.0" encoding="utf-8"?>
<methodCall>
<methodName>level.up</methodName>
<params>
<param>
<value><integer>40</integer></value>
</param>
</params>
</methodCall>
A fenti példában egy POST kérést küldünk a /xml-rpc végpontra, a fent látható XML request body-val. Jól látszi, hogy a level.up metódust szeretnénk meghívni egy integer paraméterrel. A gyakorlatban ez egyszerűen egy osztály egy adott metódusára mappelődik, hasonlóképpen:
1
2
3
4
5
class Level {
public function up($level) {
// black magic, ezt már az API használója nem tudja mit hogyan csinál
}
}
A fenti kérésre, miutá a handler osztályunk feldolgozta azt, hasonló válasz érkezhet:
1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Content-Type: text/xml
<?xml version="1.0" encoding="utf-8"?>
<methodResponse>
<params>
<param>
<value><boolean>true</boolean></value>
</param>
</params>
</methodResponse>
Látjuk, hogy a szintlépés sikeres volt, bármit is jelentsen az a mostani példában 🙂 A lényeg, hogy hasonlóképpen működik, mintha csak a kódunkban hívnánk meg egy osztályunk egy metódusát, csak ezt egy távoli gépen tesszük, ennélfogva erőforrásigényesebb és lassabb lesz az.
Akkor nézzük, hogy összefoglalva mit tudunk az RPC-ről?
- Egy végponton át, többféle művelet
- POST kéréseket használ
- Struktúrált request/response
- Nincs HTTP caching, a HTTP válaszkódból nem állapítható meg, hogyha hiba volt, mindenképp vizsgálni kell azt
- Nem használja ki a HTTP protokoll lehetőségeit
REST
A REpresentational State Transfer egy teljesen más megközelítése a dolgoknak. A lényege, hogy itt az adatbázisban szereplő entitások reprezentációja közlekedik. A HTTP protokollra épül, ezáltal próbálja annak minden szolgáltatását kihasználni, úgy mint:
- Több végpont, minden URI egyedileg azonosítja az erőforrásokat.
- A HTTP protokoll több metódusát használja
- A kliensek megadhatják az általuk használt formátumot
- Összekapcsolhatunk erőforrásokat, ezzel jelezve a kapcsolatot köztük
- Alkalmazza az erőforrások cache-elését
- Az egyes műveletek során linkeket is biztosít, hogy a kliens tudja mit is tud tenni ezután
Az RPC-vel szemben ez csupán iránymutatás, nincs kőbe vésve, hogy is kell mindezt implementálni, így egy REST API tervezésekor döntések tömkelegét kell meghoznunk:
- Milyen formában fogjuk reprezentálni az adatainkat?
- Ha egy kérést nem tudunk teljesíteni, akkor azt hogy közöljük a klienssel?
- Ha valami hiba történt, ezt milyen formában adjuk tovább, milyen HTTP status code-okkal?
- Hogy fogunk authentikálni? A HTTP stateless protokoll és habár a session sütik segítségünkre vannak a mindennapi böngészés során, de ezek használata itt nem javallott. Tehát HTTP-vel, OAuth-al vagy API tokennel fogunk authentikálni?
Pont emiatt a lazaság miatt, a REST rendkívül rugalmas és bővíthető, habár ugyanezért elég sok feladatot ró a fejlesztőre, hogy ezeket “megálmodja”.
Az előző cikkemben egy hibrid mobilapplikációt készítettünk, ami statikus adatokat használt. Most jöjjön az, hogy megírjuk a hozzá tartozó backendet, hogy valahol az ottani módosításokat letároljuk. Az egyszerűtől fogunk indulni, szimplán todo-kat szolgálunk ki, lehetővé tesszük azok módosítását, törlését, hozzáadását. Azután bevezetünk egy OAuth2-es authentikációt, az egyes todokat listába szervezzük, a listákat emberekhez rendeljük, ahogy azt a Wunderlist is csinálja.
Apigility
A fentiekhez nem mást, mint a Zend csapata által készített Apigility-t fogjuk használni. Ez egy webes API builder tool, amivel könnyedén tudjuk összekattintgatni az API nagy részét, ezáltal sok terhet levesz a vállunkról. Ráadásul nem csak Zend keretrendszerbe tudjuk a kapott kódot beilleszteni, hanem máshova is.
Kezdjük azzal, hogy letöltjük azt innen.
Csomagoljuk ki valahova és vagy állítsunk a public mappára egy VHOST-ot, vagy szimplán a projekt gyökeréből indítsunk egy PHP-s built-in webszervert:
1
$ php -S 0.0.0.0:8888 -t public public/index.php
Ezután csapjuk fel a localhost:8888-at és nézzük miből élünk!
A module mappára adjunk írási jogot a webszerver felhasználójának, különben a scaffolding nem fog menni!
A felületen fogad pár menüpont felül:
- Content negotiation: itt lehet testreszabni az általunk kezelt formátumokat, hogy mely content-type-ra, mivel is reagáljunk.
- Authentication : itt lehet felvenni/szerkeszteni az authentikációs adapterjeinket
- Database : ha már meglévő adatbázishoz kapcsolódunk, itt tudjuk felvenni az ahhoz tartozó kapcsolatot (ez fontos lesz majd nekünk)
- Documentation : a generált/általunk kitöltött adatok alapján összeállított API dokumentációt találhatjuk itt.
- Package: az elkészült API-t itt tudjuk valamilyen formában becsomagolni a későbbi deployra
- About: az aminek látszik
A sidebaron láthatjuk, hogy fel tudunk venni új API-t, ezért hozzunk is létre egyet. Ez a module mappában fog létrehozni egy modult a számunkra és ezt tudjuk majd később becsomagolni. Legyen a neve mondjuk TodoBackend.
Itt láthatjuk az API-t védő authentikációt, a hozzá tartozó REST és RPC szolgáltatásokat (egyelőre 0), valamint egy igen fontos dolgot, mégpedig a verziót. Ez fontos lehet, ha supportálni akarunk régebbi klienseket is, ahogy az alkalmazás változik.
Ha ezzel kész vagyunk, akkor jöjjön az, ami igazán meggyorsíthatja majd a dolgunkat!
Először is hozzunk létre egy adatbázist és adjunk hozzá egy felhasználót a megfelelő jogosultságokkal!
1
2
3
CREATE DATABASE todo COLLATE utf8_hungarian_ci;
CREATE USER 'todo'@'localhost' IDENTIFIED BY 'password';
GRANT ALL ON todo.* TO 'todo'@'localhost';
Hozzuk létre a todos táblát: