REST-Schnittstelle in JavaScript mit Node.js

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
    }
  ]
}
Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s