Ich probiere gerade diverse MVVM-Frameworks (z.B. Knockout) aus und bei den Beispielen werden oft JSON-Daten mit Servern über REST-Schnittstellen ausgetauscht. Jetzt wird es also Zeit für einen eigenen kleinen Server, bei dem wir mit einfachen HTTP-Aufrufen Daten abfragen (GET), anlegen (POST), aktualisieren (PUT) und löschen (DELETE) können. In unserem Beispiel werden wir Bewertungen (Ratings) über folgende API verwalten:
Methode | Pfad | Beschreibung |
---|---|---|
GET | /ratings | Liste aller Bewertungen abfragen |
GET | /ratings/2 | Bewertung mit der Id 2 abfragen |
POST | /ratings | Erstellt eine neue Bewertung |
PUT | /ratings/2 | Aktualisiert die Bewertung mit der Id 2 |
DELETE | /ratings/2 | Löscht die Bewertung mit der Id 2 |
Mir liegt ja eigentlich die Programmiersprache Java am besten, aber einen entsprechenden REST-Server in Java aufzusetzen (beispielsweise mit Spring-MVC und Tomcat) war mir etwas zu umständlich. Mit Ruby on Rails ist man sicherlich flotter am Start, wobei der Trend ja zu JavaScript auf dem Server liegt. Also Node.js schnell mal per Homebrew (bei MacOS) installieren.
brew update brew install nodejs brew info nodejs brew install npm brew info npm
‚Hello World‘ mit Node.js
Anschließend ein neues Verzeichnis erstellen, das für uns sehr nützliche Modul ‚Express‘ mit dem Paketmanager für Node.js (NPM) installieren und die JavaScript-Datei 'server.js'
für einen ‚Hello World‘- Server anlegen:
mkdir example-nodejs-rest cd example-nodejs-rest npm install express touch server.js
var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(4000, '127.0.0.1'); console.log('Server running at http://127.0.0.1:4000/');
Wenn wir jetzt den Server mit 'node server.js'
starten, dann sollten wir die URL 'http://localhost:4000/'
per Browser oder 'curl'
aufrufen können (das Beispiel startet den Server im Hintergrund und beendet ihn per 'kill'
):
bruno$ node server.js& [1] 87970 Server running at http://127.0.0.1:4000/ bruno$ curl http://127.0.0.1:4000/ Hello World bruno$ kill 87970
Mit dem Modul ‚Express‘ auf Anfragen reagieren
Um mit wenig Programmcode auf GET-, POST-, PUT- und DELETE-Abfragen eingehen zu können, bietet sich das ‚Express‚-Modul an. Der folgende Programmcode unterstützt alle Anfragen unserer API und gibt die Anfragen auf der Konsole aus.
function respondToRequest(request, response) { console.log(request.method + " -> " + request.url); response.send(200); }; var express = require('express'); var server = express(); server.configure(function() { server.use(express.bodyParser()); }); server.get('/ratings', function (request, response) { respondToRequest(request, response); }); server.get('/ratings/:id', function (request, response) { respondToRequest(request, response); }); server.post('/ratings', function (request, response) { respondToRequest(request, response); }); server.put('/ratings/:id', function (request, response) { respondToRequest(request, response); }); server.delete('/ratings/:id', function (request, response) { respondToRequest(request, response); }); server.listen(4000); console.log("Started server: http://127.0.0.1:4000");
curl
-Aufrufe und die entsprechenden Ausgaben unseres REST-Servers:
bruno$ curl http://localhost:4000/ratings GET -> /ratings bruno$ curl http://localhost:4000/ratings/2 GET -> /ratings/2 bruno$ curl -X POST http://localhost:4000/ratings -H "Content-Type: application/json" -d '{"label": "The rock", "score": 7}' POST -> /ratings bruno$ curl -X PUT http://localhost:4000/ratings/2 -H "Content-Type: application/json" -d '{"label": "The rock", "score": 8}' PUT -> /ratings/2 bruno$ curl -X DELETE http://localhost:4000/ratings/2 DELETE -> /ratings/2
Bewertungen verwalten
Unsere REST-API kann also jetzt schon aufgerufen werden. Als Nächstes benötigen wir einen Service, der für die Verwaltung der Bewertungen verantwortlich ist. Normalerweise würden wir dazu eine Datenbank einsetzen, aber für dieses einfache Beispiel merken wir uns die Bewertungen einfach in einem assoziativen Array.
Der folgende Service 'RatingRepository'
ist übrigens vom Aufbau an das Modul-Beispiel von Colin O’Dell angelehnt. Damit erhalten wir eine gute Kapselung der Daten und eine saubere Methoden-Schnittstelle.
var ratingRepository = (function() { var ratings = {}; var nextId = 0; return { findAll: function() { var ratingValues = new Array(); for (var key in ratings) { ratingValues.push(ratings[key]); } return ratingValues; }, find: function(id) { return ratings[id]; }, create: function(label, score) { var rating = { id: nextId, label: label, score: score }; ratings[rating.id] = rating; nextId++; }, update: function(id, label, score) { rating = ratings[id]; rating.label = label; rating.score = score; }, delete: function(id) { delete ratings[id]; } } })();
REST-Schnittstelle und RatingRepository zusammenführen
Zu guter Letzt müssen wir noch die REST-Schnittstelle und das RatingRepository zusammenführen. Dazu benötigen wir nur ein bisschen JSON-Verarbeitung und den passenden HTTP-Fehlercode 404, falls eine angefragte Bewertung nicht vorhanden ist. Das gesamte Beispiel kann auch unter GitHub heruntergeladen werden: https://github.com/me4bruno/blog-examples -> example-nodejs-rest):
function respondToRequest(request, response) { console.log(request.method + " -> " + request.url); response.send(200); }; var express = require('express'); var server = express(); server.configure(function() { server.use(express.bodyParser()); }); server.get('/ratings', function (request, response) { response.json({ratings: ratingRepository.findAll()}); }); server.get('/ratings/:id', function (request, response) { var rating = ratingRepository.find(request.params.id); if (rating != null) { response.json(rating); } else { response.send(404); } }); server.post('/ratings', function (request, response) { var rating = request.body; ratingRepository.create(rating.label, rating.score); response.send(200); }); server.put('/ratings/:id', function (request, response) { var storedRating = ratingRepository.find(request.params.id); if (storedRating != null) { var requestRating = request.body; ratingRepository.update(storedRating.id, requestRating.label, requestRating.score); response.send(200); } else { response.send(404); } }); server.delete('/ratings/:id', function (request, response) { var rating = ratingRepository.find(request.params.id); if (rating != null) { ratingRepository.delete(request.params.id); response.send(200); } else { response.send(404); } }); server.listen(4000); // see: http://www.unleashed-technologies.com/blog/2010/12/09/introduction-javascript-module-design-pattern var ratingRepository = (function() { var ratings = {}; var nextId = 0; return { findAll: function() { var ratingValues = new Array(); for (var key in ratings) { ratingValues.push(ratings[key]); } return ratingValues; }, find: function(id) { return ratings[id]; }, create: function(label, score) { var rating = { id: nextId, label: label, score: score }; ratings[rating.id] = rating; nextId++; }, update: function(id, label, score) { rating = ratings[id]; rating.label = label; rating.score = score; }, delete: function(id) { delete ratings[id]; } } })(); ratingRepository.create("The Dark Knight", 9); ratingRepository.create("Machete", 6); ratingRepository.create("Die Hard", 8); console.log("Started server: http://127.0.0.1:4000");
Das ist dann auch schon alles. Mit Java wäre der REST-Server bestimmt etwas umständlicher umzusetzen gewesen. Falls übrigens mal Probleme beim Starten von node
auftreten (Error: listen EADDRINUSE
), dann läuft noch ein node-Server. Mit 'ps aux | grep node'
finden wir den Prozess und können ihn mit kill
beenden.
Ach so – als Beweis, dass auch die HTTP-Fehlercodes korrekt funktionieren, sollten wir curl
mit der Option '-i'
aufrufen…
GET: Bewertungen abfragen
bruno$ curl -i http://localhost:4000/ratings HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json Content-Length: 243 Date: Tue, 09 Jul 2013 19:47:31 GMT Connection: keep-alive { "ratings": [ { "id": 0, "label": "The Dark Knight", "score": 9 }, { "id": 1, "label": "Machete", "score": 6 }, { "id": 2, "label": "Die Hard", "score": 8 } ] } bruno$ curl -i http://localhost:4000/ratings/3 HTTP/1.1 404 Not Found X-Powered-By: Express Content-Type: text/plain Content-Length: 9 Date: Tue, 09 Jul 2013 19:48:24 GMT Connection: keep-alive bruno$ curl -i http://localhost:4000/ratings/2 HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json Content-Length: 50 Date: Tue, 09 Jul 2013 19:48:32 GMT Connection: keep-alive { "id": 2, "label": "Die Hard", "score": 8 }
POST: Bewertung erstellen
bruno$ curl -i -X POST http://localhost:4000/ratings --data '{"label": "The Rock", "score": 7}' -H "Content-Type: application/json" HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/plain Content-Length: 2 Date: Tue, 09 Jul 2013 19:49:26 GMT Connection: keep-alive bruno$ curl -i http://localhost:4000/ratings HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json Content-Length: 315 Date: Tue, 09 Jul 2013 19:49:32 GMT Connection: keep-alive { "ratings": [ { "id": 0, "label": "The Dark Knight", "score": 9 }, { "id": 1, "label": "Machete", "score": 6 }, { "id": 2, "label": "Die Hard", "score": 8 }, { "id": 3, "label": "The Rock", "score": 7 } ] }
PUT: Bewertung aktualisieren
bruno$ curl -i -X PUT http://localhost:4000/ratings/5 -d '{"label": "The rock", "score": 8}' -H "Content-Type: application/json" HTTP/1.1 404 Not Found X-Powered-By: Express Content-Type: text/plain Content-Length: 9 Date: Tue, 09 Jul 2013 19:51:13 GMT Connection: keep-alive bruno$ curl -i -X PUT http://localhost:4000/ratings/3 -d '{"label": "The rock", "score": 8}' -H "Content-Type: application/json" HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/plain Content-Length: 2 Date: Tue, 09 Jul 2013 19:51:26 GMT Connection: keep-alive bruno$ curl -i http://localhost:4000/ratings/3 HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json Content-Length: 50 Date: Tue, 09 Jul 2013 19:48:32 GMT Connection: keep-alive { "id": 3, "label": "The rock", "score": 8 }
DELETE: Bewertung löschen
bruno$ curl -i -X DELETE http://localhost:4000/ratings/5 HTTP/1.1 404 Not Found X-Powered-By: Express Content-Type: text/plain Content-Length: 9 Date: Tue, 09 Jul 2013 19:52:33 GMT Connection: keep-alive bruno$ curl -i -X DELETE http://localhost:4000/ratings/2 HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/plain Content-Length: 2 Date: Tue, 09 Jul 2013 19:52:28 GMT Connection: keep-alive bruno$ curl -i http://localhost:4000/ratings HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json Content-Length: 243 Date: Tue, 09 Jul 2013 19:52:44 GMT Connection: keep-alive { "ratings": [ { "id": 0, "label": "The Dark Knight", "score": 9 }, { "id": 1, "label": "Machete", "score": 6 }, { "id": 3, "label": "The rock", "score": 8 } ] }