nodejs-1024x768

Business Applications mit Node.js und Express

Von am 06.11.2013

Hat man in Node.js eine größere Web Applikation in Planung, wird man sich früher oder später um die Applikationsarchitektur kümmern müssen. Für Node gibt es hierfür das Express Framework, welches dem Entwickler beispielsweise den Umgang mit Requests/Responses oder dem Routing erleichtert, jedoch ist die Standardarchitektur einer Express App in manchen Fällen etwas zu schlank. Dieser Artikel zeigt, wie man die von Express.js generierte Standardstruktur sinnvoll erweitern und die Applikation durch Komponenten wie Controller oder Models ergänzen kann.

Installieren von Express

Installiert man Express.js als globales Modul über `npm install -g express` kann man mit `express express-skeleton` ein Skeleton – sprich eine automatische Ordnerstruktur für die Applikation erstellen. Diese sieht wie folgt aus:

   - app.js
   - package.json
   + public
        + images
        + javascripts
        + stylesheets
             - style.css
   + routes
        - index.js
        - user.js
   + views
        - index.jade
        - layout.jade

Man erkennt schnell, dass bis zur Implementierung des MVC Musters noch ein wenig Arbeit erforderlich ist. Express vermischt hier das Routing direkt mit dem Controller Pattern, in dem es die Routes direkt in `app.js` anführt und dort auf die reinen Controller-Actions verweist.

// app.js
app.get('/', routes.index);

// routes/index.js
exports.index = function(req, res) {
   res.render('index', { title: 'Express' });
};

Applikationskonfiguration

Um die Applikation etwas flexibler zu machen, werden wir vorerst in Rootebene des Projekts einen neuen `config` Ordner erstellen und eine Datei namens `app.js` anlegen. Diese Datei wird unsere allgemeine Applikationskonfiguration beinhalten. Klassisch sind Sprache und ein CSRF Token falls man das Session Modul von Express verwendet. Das Modul besteht aus einem einfachen Objekt:

// config/app.js
module.exports = {
   language: 'en',
   session: {
      secret: 'mySessionSecret123'
   }
   // other configuration ...
}

Importiert wird die Konfiguration einfach per:

// app.js
var config = require('./config/app.js');

Controller und Routing

Die erste sinnvolle Erweiterung wäre es die Routes im `routes` Verzeichnis in Controller umzuwandeln, welche einfach dem JavaScript Konstruktor und Modul Pattern folgen. Man könnte den Controller auch als Objekt implementieren, der entscheidende Vorteil einer Funktion ist jedoch die Kapselung und die Möglichkeit andere Objekte über die Funktionsparameter zu injizieren. Im ersten Schritt benennen wir also das `routes` Verzeichnis in `controllers` um und bearbeiten die `index.js` wie folgt:

// controllers/index.js
module.exports = function () {
   // possible attributes or requires ...

   return {
      rootAction: function (req, res) {
         res.render('index', { title: 'Express Advanced' });
      }
   };
}

Der erste signifikante Vorteil hier ist, dass Attribute definiert werden können, die nur dem Controller zugeordnet sind. Hier können Konfigurationen oder beispielsweise ORM EntitiyManager geladen und initialisiert werden. Dem Controller können außerdem, wie bereits beschrieben, per Dependency Injection Objekte direkt in die (jetzt gerade leeren) Funktionsparameter injiziert werden.

Nun wäre es an der Reihe die Routendefinitionen aus der `app.js` in eine Routen-Konfigurationsdatei outzusourcen. Wir erstellen also in unserem `config` Ordner eine `routes.js` Datei. Diese Datei beinhaltet nun alle unsere Routes, welche auf unsere Controller verweisen:

// config/routes.js
module.exports = function () {
   // get required controllers
   var controller = {
      'index': require('../controllers/index.js')()
   };

   // define routes
   app.get('/', controller.index.rootAction);
   app.get('/blog', controller.index.blogAction);
   app.get('/login', controller.index.loginAction);
   app.post('/login', controller.index.checkLoginAction)
};

In der `app.js` importiert man die Konfigurationsdatei mit den Routes einfach mit:

// app.js
require('./config/routes.js')();

Models und Entities

Applikationslogik landet überlicherweise im Business- bzw. Domänenlayer und nicht im Controller selbst. Aus diesem Grund erstellen wir einen `models` Ordner in unserer Rootebene des Projekts. Darin erstellen wir direkt ein Unterverzeichnis namens `entities`, welches unsere Entitäten beinhalten wird. Solche Entitäten bilden unsere Domänenobjekte bzw. Datenbanktabellen in “Objektform” ab. Alle anderen Implementierungen der Logik landen direkt im `models` Verzeichnis. Ein solches Domänenobjekt könnte folgendermaßen aussehen:

// models/entities/user.js
module.exports = function (/* possible EM or DAO injection */) {
   // EM or DAO ...

   return function () {
      // private attributes
      var _id = null,
          _username = null,
          _password = null;

      // constructor
      (function construct() {
          // ...
      }());

      // private methods
      function privateMethod () {}

      // return public object with public functions
      return {
         // typical getter for all attributes
         getPropertyObject: function () {
            return {
               id: _id,
               username: _username,
               password: _password
            };
         },
         setPropertyObject: function (propertyObject) {
            _id = propertyObject.id;
            _username = propertyObject.username;
            _password = propertyObject.password;
         },
         loadUser: function (username, password) {
            // use DAO with ORM or implement manually
         },
         persistUser: function () {
            // use DAO with ORM or implement manually
         }
      };
   };
}

Wie man am obigen Code erkennen kann, ist es ganz einfach ein Domänenobjekt/Entity zu erstellen. Auffallend ist hier, dass das Model extra noch einmal in einer Funktion gewrappt ist, welche dann zurückgegeben wird. Vorteil dieser Implementierung ist ein eigener Scope für Entity Managers oder Data Access Objects, da diese normalerweise nicht als Attribute des Models abgebildet werden sollten. Objekte von diesem Model können dann ganz bequem aus einem Controller erstellt werden:

// controllers/index.js
module.exports = function (/* EM or DAO */) {
   // require model
   var User = require('../models/entities/user.js')(/* EM or DAO */);

   return {
      // ...
      checkLoginAction: function (req, res) {
         var usernameGiven = req.body.username,
             passwordGiven = req.body.password;

         var user = new User();
         user.loadUser(usernameGiven, passwordGiven);
      }
   };
};

Views mit Underscore

Wer mit Jade als Templateengine nicht zufrieden ist, kann auch jede andere verwenden, die mit Express kompatibel ist. Für mich hat die Templateengine von Underscore gewonnen, da ich normale HTML Dateien als Views verwenden kann und dort meine minimale Logik integrieren kann. Wer ebenfalls Underscore verwenden will, muss seine `package.json` Datei um zwei Module erweitern, 1. Underscore und 2. Consolidate (wird benötigt um Underscore in Express zu integrieren):

// package.json
{
   "name": "myApp",
   "version": "0.0.1",
   "private": true,
   "scripts": {
      "start": "node app.js"
   },
   "dependencies": {
      "express": "3.4.0",
      "underscore": "*",
      "consolidate": "*"
   }
}

Danach sollte man nochmal `npm install` ausführen, um die Node Module, die noch fehlen, nachzuinstallieren (in diesem Fall Underscore und Consolidate). In der `app.js` muss man danach lediglich noch die View Engine updaten:

// app.js
var cons = require('consolidate');

app.engine('html', cons.underscore);
app.set('view engine', 'html');

Fazit

Express liefert ein sehr schlankes und umfangreiches Framework für Web Applikationen. Man kann mit Leichtigkeit Requests und Reponses bearbeiten, Routen definieren, Sessions und Cookies verwalten und vieles mehr. Durch die Flexibilität des Frameworks ist es auch sehr einfach zu erweitern. Express zwingt den Entwickler nicht sich an die Struktur zu halten, sondern erlaubt die Funktionen in eigenen, neuen und erweiterten Architekturen einzusetzen.

7 Kommentare

  • Aleks am 10.11.2013 um 02:08

    Ach ja, ganz vergessen weil du Twitter erwähnt hast – Facebook verwendet ja immer noch PHP. Klar die nutzen Hiphop um es in C++ umzuwandeln und zu kompilieren, aber der Businesslayer und die Models sind trotzdem in einer dynamisch typisierten Sprache geschrieben.

  • Aleks am 10.11.2013 um 02:01

    Gut dann komme ich und sage Linkedin hat von Rails auf Node gewechselt und ist laut Benchmarks bis zu 20x schneller :P. Nein ich versteh deinen Standpunkt total und bin ganz bei dir! Ich bin aber auch optimistisch und sehr gespannt wohin es Node im Backend noch verschlagen wird ;).

  • Christoph am 09.11.2013 um 20:48

    Ich nehme mal an, wenn deine Firma weiter skalieren wollte, würde sie sich das mit einem C# Backbone noch einmal überlegen.

  • Christoph am 09.11.2013 um 20:46

    Ich will keinem irgendwas aufzwängen, alle Entscheidungen sollten nur gut überlegt und begründbar sein. Entwickler bekommen schnell den Tunnelblick, wenn es um Technologien geht und setzen eine einzelne Technologie (Java oder JavaScript) für alles ein. Also immer schön die Augen offen halten. 😉 btw Twitter hat von Ruby-on-Rails auf Java umgestellt, ist jetzt 3x so schnell und kann 10x so viele Anfragen bewältigen.

  • Aleks am 09.11.2013 um 12:59

    Meine aktuelle Firma z.B. betreibt nicht mal einen IIS und ein Java Backbone gibt es auch nicht wo man sagen könnte da hätte man bereits gute Schnittstellen 🙂

  • Aleks am 09.11.2013 um 12:52

    Natürlich bieten sich hier Sprachen wie C# sehr gut an, aber ich denke das kommt dennoch ganz auf den Use Case, die Projektart und den Entwickler an. Würden alle Webprojekte auf C# im Backend basieren, hätten wir beispielsweise keine MVC Frameworks für PHP, und doch haben Projekte wie Symfony mit z.B. Doctrine als ORM gezeigt, dass es sehr wohl möglich ist hier einen sauberen Domänenlayer aufzubauen!

  • Christoph am 07.11.2013 um 11:03

    Interessanter Artikel. Ich möchte jedoch dazu raten, sich den Schritt zu dynamisch typisierten Sprachen wie JavaScript im Businesslayer sehr genau zu überlegen. JavaScript ist nicht für alles optimal. Wenn es um die Domäne geht, bieten strikte Sprachen wie Java und C# immer noch die höchste Sicherheit und Performance.

The comments are closed.