N4JS Tutorial
The flexibility of JavaScript with the type safety of Java
N4JS is a language and IDE designed for developers who need to build scalable projects whose code should be modular, reusable and easily maintainable over time. N4JS bridges the strengths of ECMAScript (also known as JavaScript) and Java. The result is a typed JavaScript superset that is dynamic, flexible and type-safe.
Experience with JavaScript or Java?
Developers who are experienced with JavaScript can easily apply their knowledge in N4JS. As N4JS is a typed superset of ECMAScript, it is possible to use plain ECMAScript in N4JS. Most of the important features from ECMAScript 2015 are supported, such as modules with import and export, classes, iterables and destructuring of arrays and objects. Developers with Java experience will be familiar with features from Java 8 such as nominal typing, interfaces and generic classes and methods; even dependency injection and testing look similar.
Development Environment
N4JS has an IDE based on Eclipse. It comes with its own workspace, libraries and plugins. The IDE has a transpiler (i.e. a compiler that translates from one language to another) which validates N4JS code and then transforms it to JavaScript. The transpiler also performs imports from ECMAScript to N4JS. The benefit of type safety means that errors in code are noticed while editing in the IDE, allowing for problem-solving before code is ever deployed.
Developers who have previous experience using Eclipse will be familiar with most of the common UI features such as Editor, Project Outline, Project Explorer and Console views. The arrangement of views can be easily selected using the perspectives icons as highlighted in the upper-right of the window.
Using N4JS to Implement a Node.js Application
The following screenshot shows the most important views of the IDE:
![N4JS IDE](images/ide.png)
In order to illustrate some of the features of N4JS, we shall look at building a Node.js-based server of a Task Manager which serves as a reminder or to-do list application. As we build this basic example, we shall add improvements step-by-step, introducing new features with each increment in order to demonstrate some of the strengths of the language and IDE through a simple practical model.
Before we dive in and start creating our example, it is worth briefly describing the architecture of our application in order to have a considered approach to the concept. In this particular example, we can think of a multi-tiered client-server structure. The idea here is that when we have the application separated, each tier can be reused for different purposes.
We will start with modeling and implementing the domain model before implementing a first version of the storage tier which will enable our first tests. The controller is then built using a test-driven approach. We later improve the storage tier using a real database before adding a REST API with Express and, finally, a simple Web UI.
Creating a new N4JS Project
Let’s begin by creating a new N4JS Project that contains our whole application. In case of larger applications, we might want to create projects for each component (or tier) of the application. In order to keep this example small, we will use only one project.
The "New N4JS Project" wizard can be accessed at ⌘+n for Mac, Alt+Shift+n on Windows)
(Or using the keyboard shortcuts![New Project Wizard](images/newprojectwizard.png)
When we select Finish, a new project folder is created with the following content:
-
folder src: contains the .n4js source files for the project.
-
folder src-gen: contains the transpiled JavaScript that is generated after compiling n4js files. The IDE automatically transpiles n4js files on save.
-
manifest.n4mf: project description file, containing description of dependencies.
The Domain Model
Before we start implementing the domain model, we shall take a look at a UML diagram of it:
The UML diagram illustrates how the entities of our domain model are related and which features the classes of the model will have. We will begin by defining Task which is extended by Appointment and Todo.
Creating new classes and modules
In our new N4JS project, we can begin creating our entities. The easiest way to do this is to utilize the New N4JS Class wizard, accessed via
![newclasswizard](images/newclasswizard.png)
N4JS supports modules introduced by ECMAScript 2015. A module contains functions, classes and other declarations and code. The declared elements can be exported and imported by other modules. We will use a single module for all classes of our domain model. In larger projects, one might follow the Java convention to create a single file per class.
The module specifier is the full path name of the module file, relative to the source folder. We use Unix-like path separators, that is forward slashes "/". In the Class Wizard (and later in import statements) we will omit the file extension.
In the above figure, we are creating a new class named Task which will be saved in the module model in the project source folder.
Implement the entity classes
The class wizard has already created a file and the empty class "Task". We will manually enhance this class as follows:
export abstract class Task { (1)
public id: string?;
public label: string?;
}
1 | In the first line of code, we have defined an abstract class named Task. Classes in N4JS are similar to classes in ECMAScript 2015 or Java. The concept of an abstract class is borrowed from Java. This means that we cannot have a direct instance of Task in our model, but we may have subclasses of Task. |
We are populating the class with fields which are simple data fields of the class. This is also borrowed from Java. The transpiler will move the field (with possible initializers) into the constructor. The two data fields of Task are id and label which we have annotated with types. The N4JS transpiler will later remove these type annotations.
The type annotations declare the type of the data field. The type checker will issue errors if we later assign values of a non-compatible type to the variables. The question mark ? is a type modifier declaring the value as optional. That means that a new Task may or may not have values assigned for their id and label.
N4JS also provides the concept of access modifiers similar to Java with the modifiers public, protected, project and private. Access modifiers constrain the visibility of elements, that is, they restrict from where an element can be accessed.
-
public
means that the element can be accessed from everywhere, -
protected
may only be accessed from subclasses, -
project
only from within the project (this is the default visibility) -
private
accessed only from within the same module.
We now manually add two classes to the same file:
/** An appointment. */ (1)
export class Appointment extends Task { (2)
public time: Date; (3)
public place: string; (3)
}
export class Todo extends Task {
public dueDate: Date?;
public done = false; (4)
}
1 | Appointment is prefaced with a JSDoc annotation (a comment beginning with /** and closed with */ which documents that it is a single Appointment task. JSDoc comments are used to provide markup annotation of your code. The content of these comments is displayed when you hover over a reference to that element. |
2 | The class Appointment is a subclass of Task. This is achieved by using the keyword extends and demonstrates inheritance. N4JS supports single class inheritance similar to ECMAScript 2015 or Java. The subclass Appointment will inherit the members id and label with types from Task. |
3 | We add the members time and place with type annotations. The type annotations of the members in Appointment are more strict than before, since the optional modifier (denoted by the question mark) is missing. We will see the effects of these different modifiers later on. |
4 | Class Todo has a data field without type annotation but with an initializer. The N4JS type checker can infer the type of the initializer, in this case a boolean literal, and implicitly sets the type of the field to boolean. |
In all classes we have defined above, we are using the ECMAScript 2015 export keyword so that Appointment and Task can be imported and reused in other modules. As the project grows, the benefits of having individual, reusable modules become more and more useful in that they can be imported into other modules or even other projects.
Add an Enumeration
We also want to add a priority field to the Todo
class. We will modify the
Todo
class and add a new type Priority
as follows:
export class Todo extends Task {
public dueDate: Date?;
public priority = Priority.NORMAL;
public done = false;
}
@StringBased (1)
export enum Priority {
LOW, NORMAL, HIGH
}
1 | Use string-based enum to simplify (de-)serialization |
Enumerations allow us to represent a fixed set of constants: LOW
, NORMAL
and HIGH
.
The reason we use an enumeration here is because we know all possible values for Priority
at compile-time so we may limit it to these constants. N4JS provides two kinds of
enumerations: "ordinary" and "string-based". The former
will be translated to objects, enabling extended reflection (for example to get the type of
the enumeration or get all literals). The latter will be translated to strings. Literals of
string-based enumerations are, in fact, represented as plain strings in the JavaScript
output code. As a result, they offer less reflection capabilities.
The Storage Layer
Let’s first have a look at the UML diagram describing out storage tier:
The storage tier is responsible for persisting our entities. We will create two different kinds of "persistence". We will start with a simple in-memory peristence, which can be used for testing. Later on, we will add a proper persistence layer using MongoDB.
Defining an Interface
We can create the storage module with the New Class wizard. We call it "Storage.n4js", following Java’s convention of naming the module similar to the contained class or interface. We will manually edit the file as follows:
import { Task } from "model"
export public interface Storage {
size(): int
clear()
getTasks(): Array<Task>
storeTask(task: Task): string
isEmpty(): boolean {
return (this.size()) === 0;
}
}
The very first line of code displays ECMAScript 2015’s import statement. It is a so-called "named import": We import the element "Task", in our case a class, from the module with the specifier "model".
This time, we do not define a class but an interface "Storage". N4JS supports interfaces which operate similar to those in Java 8. Interfaces are similar to classes, but they cannot be instantiated. In N4JS it is however possible to use the "instanceof" operator with interfaces. Usually interfaces contain abstract methods, but they can contain data fields, getters and setters as well. Similar to Java 8, interface methods can provide a default implementation. Here, we provide a default implementation for the method isEmpty. Classes implementing the interface can either rely on the default implementation or provide a more efficient one. As in Java, a class can implement multiple interfaces, and also interfaces can extend multiple interfaces.
Implement the Interface
Since we cannot instantiate an interface, we need a class implementing the interface. We will create a new module for a class called "StorageInMemory.n4js". This module will simply keep all entities in memory. If you copy-paste the following code snippet in your IDE, you will see a few errors. Do not fret, it is expected and we will deal with them shortly.
import { Storage } from "Storage"
import { Task } from "model"
export class StorageInMemory implements Storage { (2)
private lowestUnusedId = 1;
@Final
private tasks = new Map<string,Task>(); (1)
@Override
public size(): int {
return this.tasks.size;
}
@Override
public storeTask(task: Task): string {
let id = 'id' + this.lowestUnusedId++;
this.tasks.set(id, task);
task.id = id;
return id;
}
@Override
public clear() {
this.lowestUnusedId = 1;
this.tasks.clear();
}
@Override
public getTasks(): Array<Task> {
return Array.from(this.tasks.values());
}
}
1 | The IDE will show an error here! |
2 | We use the keyword implements (known from Java) to define that this class implements
the interface. We have to provide specific implementation of the methods of the Storage
interface by using the @Override annotation to define size , clear , getTasks
and storeTasks (not all methods are shown here). This annotation is similar to
the annotation used in Java. It ensures that whenever a method in the interface is
changed, the type checker can issue a warning. This can be a lifesaver when larger
projects are to be maintained over time or across several development teams. |
The above code will raise a compile error because type Map
is not available
in ECMAScript Version 5. We’ll have to tell N4JS that our example is intended
to run as ECMAScript 2015. Before doing this in the following section, let’s
first look at the other parts of the above class declaration in more detail.
We use a data field tasks
to store all the tasks in a map. The type Map stems from
ECMAScript 2015. It is a generic type similar to Array, which the observant reader may
have already seen in the Storage interface. N4JS provides support for generic types and methods
is similar to Java 8.
Edit the Manifest
By default, N4JS provides all the types known by ECMAScript 5. In order to use elements (types, functions or variables) defined by a newer JavaScript version, we have to add a corresponding runtime library as project dependency to the manifest. This has no direct effect on the compiled code, it simply tells the type checker to assume that certain types of a newer JavaScript version will be available at runtime (provided by the JavaScript engine the code is intended for).
Such meta information about an N4JS project is kept in a so-called manifest file.
We need to open the manifest.n4mf
file and edit a dependency. The default manifest
files created by the New Project wizard look like the following:
ProjectId: n4js.example.tasks
ProjectType: library
ProjectVersion: 0.0.1
VendorId: eu.mycompany
VendorName: "MyCompany AG"
Output: "src-gen"
Sources {
source {
"src"
}
}
We need to add the following section at the end. Note that the manifest editor supports content assist similar to the N4JS editor.
RequiredRuntimeLibraries {
n4js-runtime-es2015
}
This will add all additionally defined types of ECMAScript 2015. It will also add new methods to types already defined in ECMAScript 5.
Running a Module
Having created the first version of our domain model and storage tier, we are ready to try it out. For that, we create a module "Runner.n4js" with the following code:
import { StorageInMemory } from "StorageInMemory"
import { Todo } from "model"
let sim = new StorageInMemory();
let todo = new Todo();
todo.done = false;
todo.dueDate = new Date();
todo.label = "Test TODO";
sim.storeTask(todo);
console.log(sim.getTasks());
We then launch this module with Node.js. The easiest way to do that is with the context menu (accessed by right-clicking in the editor) and selecting "Launch in Node.js". as shown in the following screenshot:
![firstlaunch](images/firstlaunch.png)
This will run the module currently opened in the editor. The output will be printed to the console view, for example
![firstlaunchconsole](images/firstlaunchconsole.png)
Extend Entities with Spec-Constructor
When we look at the runner code, creating a new task is quite annoying: It has to be created with a new expression, and then every data field has to be set separately. To simplify this, we add a constructor to our base entity class Task as follows:
export abstract class Task {
// ...
constructor(@Spec spec: ~i~this=undefined) {
// code for initialization will be generated due to @Spec annotation
}
}
...
The concept of constructors is taken from ECMAScript 2015. However, the parameter is very special
to N4JS. We briefly describe the type expression ~i~this=undefined
used here. The part =undefined
declares
the default initializer of the parameter spec
(c.f. ECMAScript 2015 cite:[ECMA11a]). Parameters which have default
initializers are optional, hence N4JS allows for omitting them in function calls (or in case of
constructors, in the NewExpression
).
this
is a known keyword in ECMAScript, it usually refers to the receiver of a property or, in case of classes,
method call. But here we use it as a type expression, referring to the type of the this
keyword. This is usually the class in which the method or constructor is defined. That is,
in case of Task it will be Task
. However, we have two subclasses of Task
. We do not
define a new constructor in these classes, instead we let these classes inherit this
constructor. In case of Todo
, the this
type will become Todo
and in case of Appointment
,
Appointment
. Simply referring to the this
type wouldn’t make any sense in the constructor,
since we would need a first instance in order to create another one - but how could we
create the first one? The solution comes with the ~i~
prefix.
As discussed in the feature sheet, N4JS supports nominal and structural typing. Structural typing
is activated in N4JS with the tilde ~
. Two structural types are compatible, if they provide
the same properties, or in case of classes, public members. In the constructor, we only need to
set the fields. In N4JS, we can use ~~
to refer to the so-called field structural type. Two
field structural types are compatible, if they provide the same fields - methods are
ignored in these cases. Actually, optional fields are also ignored. This explains why we marked
some of the fields with the optional modifier. Note that fields with an initializer are also
treated as optional (since the initializer provides a default value). Actually, N4JS can do even
more. There are several modifiers to further filter the properties or members to be considered:
~r~
only considers getters or data fields, ~w~
only setters and data fields. ~i~
is used
for initializer parameters: For every setter or (non-optional) data field in the type, the
~i~
-type needs to provide at least a getter (or a readable data field). Optional fields
are also treated as optional in the field structural types.
For the concrete class Todo
, the ~i~
-type is not required to contain any property since all
its fields are either optional or have an initializer. It contains the optional fields id
,
label
, dueDate
, priority
and done
. ~i~Appointment
contains the required properties time
and
place
, and the optional fields id
and label
.
In most cases, we need this information in the constructor to set the fields accordingly. For Task we would write:
constructor(spec: ~i~this=undefined) {
this.id = spec.id;
this.label = spec.label;
}
In order to simplify the code, the annotation @Spec
tells the transpiler to add exactly this
code automatically. Even better: Since Appointment
and Todo
inherit the constructor, the
transpiler will add constructor code in these classes to set the additional fields also. That
is, with this single constructor, the @Spec
annotation and the ~i~this
type expression,
we have solved the problem of initialization for all our entity classes with a single stroke!
Using this @Spec
constructor would then look similar to this:
let todo = new Todo({dueDate: new Date(), label: "Test TODO"});
sim.storeTask(todo);
Short summary
The main concepts demonstrated so far by our example are:
-
Modules with import and export
-
Classes with inheritance and constructors
-
Interfaces with default methods
-
Enumeration
-
Special strategies for structural types
-
Manifest.n4mf file and runtime library dependencies
We can now proceed to implement a Task Manager.
Creating a Task Manager
We will now create the controller tier. This tier uses the entity and storage classes to provide functionality that is actually useful for the user of the application.
Since we eventually want to implement a REST API (and use a real database), we need to introduce asynchronous functions. So before we actually implement any controller class, we have to adjust our storage tier to support asynchronous functions.
Make the Storage Asynchronous
If we would like to use a real database, all calls to the database will be asynchronous. Asynchronous programming is a typical task in ECMAScript and there are several solutions to do this.
ECMAScript 2015 introduced a new class Promise which is supposed to be used with in these cases. Its methods accept callback functions which are called once the asynchronous event has been triggered. Since these callback functions tend to call other asynchronous functions, ECMAScript programmers easily end up in the so called 'callback hell'. There is a proposal for upcoming ECMAScript versions to use special constructs in the language to get rid of this callback hell. The idea is to mark asynchronous functions as "async" and, when these functions are called, the program can "await" the result. This async/await feature is already supported by several JavaScript frameworks and it is also built-in to N4JS including validation.
First we have to change the Storage interface and mark all methods which are supposed to be asynchronous as
async
:
import { Task } from "model"
export public interface Storage {
async size(): int (1)
async clear()
async getTasks(): Array<Task>
async storeTask(task: Task): string
async isEmpty(): boolean {
return (await this.size()) === 0;
}
}
1 | Adding "async" to the size method and without adding the "await" keyword, you will get a
warning in method isEmpty similar to the following: |
![awaitwarning](images/awaitwarning.png)
You will also get a lot of other errors in other files:
![asyncerrors](images/asyncerrors.png)
Without an async/await and type aware IDE you probably would have missed one or the other of these errors.
We can easily fix that by simply adding async
to all the indicated methods.
If you still have the runner module, you probably will get a warning there as well. If you ignore that warning and run it again, you will get the following
Promise { <pending> }
instead of the expected output. We are not going to fix this problem now as we will introduce a better way of testing the code after the next step.
Create TaskManager
import { Todo } from "model"
import { Task } from "model"
import { Storage } from "Storage"
import { StorageInMemory } from "StorageInMemory"
export public class TaskManager {
private storage: Storage = new StorageInMemory();
public async getTasks(): Array<Task> {
return await this.storage.getTasks();
}
public async createTodo(label: string): string {
let newTodo = new Todo({label: label});
let newId = await this.storage.storeTask(newTodo);
return newId;
}
}
This class does not reveal any new concepts, but how do we test it? For that, we are going to use the N4JS test framework.
By utilizing the built-in test suite, classes and modules will not become polluted with superfluous test- code. In addition, it is possible to overcome some access modifiers restrictions so there’s no need to restructure or rewrite your code specifically to run tests.
Since we use a tier architecture, it is quite easy to add a test: We simply replace one tier with appropriate tests:
Testing
Since we do not want to mix up the application with the tests, we create a new project. We use the new project wizard
![createtestproject](images/createtestproject.png)
We adjust the manifest.n4mf
accordingly:
-
Define which project we test in the TestedProjects section.
-
Change the source folder to test folder. This way the IDE knows where to look for tests later on.
-
Add project dependencies to the built-in test framework "Mangelhaft", which also provides a comprehensive collection of assert methods.
After adding these changes, the manifest of the test project will look as follows:
…
TestedProjects {
n4js.example.tasks
}
Output: "src-gen"
Sources {
test {
"src"
}
}
ProjectDependencies {
org.eclipse.n4js.mangelhaft,
org.eclipse.n4js.mangelhaft.assert
}
We can now write our first test. Again, we use the class wizard to create a module "TaskManagerTest" containing a class with the same name. The first test should look like that:
import { TaskManager } from "TaskManager"
import { Assert } from "n4/mangel/assert/Assert"
export public class TaskManagerTest {
mgr: TaskManager = new TaskManager();
@Test
async testCreateTodo() {
await this.mgr.createTodo("test todo");
Assert.equal("test todo", (await this.mgr.getTasks())[0].label);
}
}
Mangelhaft is an xUnit-like test framework. For the sake of simplicity, N4JS uses the same annotations as
the popular Java test framework JUnit. In our case, we have a single test method which needs to be annotated
with @Test
.
Since we are testing asynchronous code, the test method needs to be asynchronous as well and we need to
"await" the results of the methods we call. Mangelhaft supports asynchronous code so we do not have to
bother about that any further. This is the nice thing about using async
/await
and N4JS: asynchronous
programming becomes as simple as synchronous programming!
We can run the test via the IDE. This works similar to launching the code with Node.js by simply using the context menu. The IDE will detect a test and it will automatically add the correct menu entry to the context menu:
![testinnodejs](images/testinnodejs.png)
This will run the test and the test view will show the result of the first test:
![firsttestresults](images/firsttestresults.png)
Storage using MongoDB
Instead of "storing" the entities in memory, we want to use a real database. In this example, we are going to use MongoDB. To follow along this section on your own computer, you must have MongoDB installed and start a data base server instance via the command line as follows:
mongod --dbpath /db
In order to use MongoDB from N4JS, we need the appropriate npm package which allows MongoDB access from ECMAScript. Adding this npm and making it available in N4JS is as simple as adding any project dependency. We have to open the manifest editor (of the tasks project) and add the following project dependency:
ProjectDependencies {
mongodb
}
![quickfixnpminstall](images/quickfixnpminstall.png)
The quick-fix will automatically download all required npm packages, that is mongodb and all its dependencies. We can now use mongodb from our N4JS code.
To let N4JS know about the types a particular npm package provides, an N4JS definition file with extension .
n4jsd
is required (the same applies if you use a plain JavaScript file from N4JS). For some npm packages,
definition files are provided at github.com/NumberFour/n4jsd.
Let’s assume for the moment there are no .n4jsd
file available for MongoDB; we import MongoDB using
a dynamic import as follows:
import * as mongodb+ from "mongodb"
mongodb.MongoClient.connect('mongodb://localhost:27017/tasks', function (err: any+, db: any+) {
if (!err) {
// ... use data base ...
db.close();
}
});
However, since we do have an .n4jsd
file available, we can import types such as MongoDB
, Collection
,
or ObjectID
provided by MongoDB using an ordinary ECMAScript2015 named import, just as if we were
importing from an N4JS module:
import { Storage } from "Storage";
import { Task, Appointment, Todo } from "model";
import { Collection, Db, MongoClient, ObjectID } from "mongodb";
/**
* Persistence for task lists using a mongodb instance.
*/
export class StorageMongoDB implements Storage {
cachedDb: Db = null;
private async getTasksCollection(): Collection {
if (!this.cachedDb) {
this.cachedDb = await MongoClient.connect('mongodb://localhost:27017/tasks');
}
return this.cachedDb.collection('tasks');
}
public async shutdown() {
this.cachedDb.close(true);
this.cachedDb = null;
}
}
In the above code section, we are implementing StorageMongoDB from the Storage interface and then calling some of the standard MongoDB collection methods.
The next step is to retrieve the information about our Tasks and to store them in our MongoDB database. In
the storeTask method, we are then retrieving the inserted item id’s from MongoDB and returning them as a
task.id
.
export class StorageMongoDB implements Storage {
// ...
@Override
public async size(): int {
let coll = await this.getTasksCollection();
return await coll.count({});
}
@Override
public async clear() {
let coll = await this.getTasksCollection();
await coll.deleteMany({});
}
@Override
public async getTasks(): Array<Task> {
let coll = await this.getTasksCollection();
let resultRaw = await coll.find({}).toArray();
let result = resultRaw.map((data): Task => fromData(data)); (1)
return result;
}
@Override
public async storeTask(task: Task): string {
let coll = await this.getTasksCollection();
let result = await coll.insertOne(toData(task));
if (result.insertedCount === 1) {
task.id = result.insertedId.toHexString();
return task.id;
}
throw new Error("insert document failed");
}
}
1 | The use of the => arrow function, derived from ES6. Arrow functions have implicit lexical
binding and are less verbose than traditional function expressions. |
The above code uses two helper functions, toData()
and fromData()
. Those illustrate two techniques available
in N4JS: reflection and so-called @Spec
constructors, respectively. Reflection is known from many
languages and allows for retrieving information of a type and its members at runtime. It is used in
fromData()
as follows:
function toData(task: Task): ~~Task {
let metaClass = N4Type.of(task),
data: any+ = {};
// note: would have to set data._id, here, if we
// wanted support for updating existing tasks
data._type = metaClass.name;
let taskAsObject: Object = task; // up-cast to object to allow index access
for (let field of metaClass.dataFields(true,true)) {
data[field.name] = taskAsObject[field.name];
}
return data;
}
Conversely, @Spec
constructors are special constructors that allow us to create a new instance of a class and
initialize it with values provided by a plain data object in properties that correspond to the type’s fields.
function fromData(data: any+): Task {
let ctor = typeToCtor.get(data._type as string);
if (!ctor) {
throw new Error('Unsupported type of data model entity: ' + data._type);
}
let task = new ctor(data);
task.id = (data._id as ObjectID).toHexString();
return task;
}
const typeToCtor = new Map<string,constructor{Task}>([
['Todo', Todo] as Iterable2<string, constructor{Task}>,
['Appointment', Appointment] as Iterable2<string, constructor{Task}>
]);
Note that both functions toData
and fromData
should be declared outside of the StorageMongoDB
class but in the same file StorageMongoDB.n4js
.
By using the above two helper functions, we avoid sending our data model instances directly to the MongoDB
driver. Note that the entire implementation is intended for illustration purposes and in a real-world
systems many details would be handled differently, depending on the actual requirements.
Dependency Injection
We now have two implementations of the interface Storage. For testing, the in-memory solution is adequate, but for the application we want to use the MongoDB solution of course. Since we are using the Storage in our TaskManager class, we would need to change the TaskManager depending on the storage solution. This is inconvenient and error prone. It would be much better if we could configure which storage class to use from outside the TaskManager at some central location. This is possible with dependency injection.
To learn more about how dependency injection works, we have written an
extended feature description that describes the benefits of this technique.
In short, N4JS provides built-in support for dependency injection using the same annotations as known from JSR-330/
Google Guice. Instead of using initializers calling the constructor for certain fields, we just mark them
with @Inject
. We will do that with the storage field in the TaskManager class:
export public class TaskManager {
@Inject
private storage: Storage;
// ...
}
After removing the constructor, the IDE will create a warning that one of the imports is unused. You can easily fix that by using the "Organize Import" feature, either from the context menu (or via ⌘+Shift+O on Mac OS, Ctrl+Shift+O on Windows). |
How does N4JS now create the instance of storage? For that, we need an injector. An injector is responsible
for creating all variables annotated with @Inject
. The injector is configured with a so-called "binder".
The binder is more or less a translation table telling the injector which type it should use to create a
concrete instance when a certain type is given. In our case, we need to tell the injector whether we want an
instance of StorageInMemory or StorageMongoDB. We are going to adjust the test accordingly.
For that, we first add a binder to the test module "TaskManagerTest.n4js":
@Binder
@Bind(Storage,StorageInMemory)
class InMemoryBinder {}
The annotation @Binder
marks the class InMemoryBinder to become a binder. For each mapping we need to add
an annotation @Bind
, which takes the requested type as the first argument and the actual type as the
second one. We only actually need to define bindings for interfaces. If the requested type is a class and if
no binding is defined for it, the injector will simply create an instance of that very type.
The next step is to create an injector. Fortunately, we do not have to do that manually. The dependency
injection framework of N4JS introduces the notion of dependency injection components (DIC). A DIC is
associated with an injector; this is done by using the annotation @GenerateInjector
. Additionally we need
to tell the framework which configuration it should use for the injector, this is done via the annotation
@UseBinder
, which expects the type name of a binder class.
@GenerateInjector @UseBinder(InMemoryBinder)
export public class TaskManagerTest {
…
}
The IDE helps us in finding problems: since the TaskManager class uses injection to get the storage field, it needs to be injected itself. The IDE warns us in the TaskManagerTest class:
![injectionwarning](images/injectionwarning.png)
This is hard to find but easy to fix: We just have to replace the initalizer with an @Inject
annotation.
…
export public class TaskManagerTest {
@Inject
mgr: TaskManager;
}
With these little changes, we can now configure the storage solution from outside the TaskManager class. Running this test will behave as before but we have removed the hard-coded dependency from TaskManager to StorageInMemory.
Web UI module
The final step in building functionality into our model is to create a simple web user interface using Express as a dependency to create a web server. We will then pass our tasks from MongoDB into a small HTML page to display the results. We will create the web server using Express.
In order to use Express, we need the appropriate npm module. Adding this npm and making it available in N4JS is as simple as adding any project dependency, as already shown for MongoDB, above. We have to open the manifest editor (of the tasks project) and add the following project dependency (along with the existing dependencies):
ProjectDependencies {
mongodb,
express,
n4js.lang
}
The dependency n4js.lang
contains N4Injector
which is needed later, so make sure it is declared as a dependency.
The quickfix will automatically download all required npm packages, that is, Express and all its dependencies. We can now use Express in our N4JS code (for more details, esp. N4JS definition files, see above description on MongoDB).
//Creating a simple Web User Interface in HTML
import { TaskManager } from "TaskManager"
import {Application, Response } from "express";
import express from "express";
import { Todo } from "model"
export class WebUI {
private app: Application;
@Inject
private manager: TaskManager;
public start() {
this.app = express();
this.app.get('/', async (req, res) => {
let page = await this.renderHomePage();
res.send(page);
});
}
}
Express is a web framework that provides (among other things) HTTP helpers for web routing. In the above
example, we are importing the classes Application
and Response
from Express and creating a home page
that we can render some HTML to. Next we will add a method for creating new tasks:
public start() {
// ... code shown above ...
this.app.get("/create", async (req, res) => {
let values = req.query as ~Object with {type: string, label: string};
if (values && values.type === 'Todo' && values.label && values.label.length > 0) {
await this.manager.createTodo(values.label);
}
redirect(res, '/');
});
}
Have Express listen on port 4000 at localhost:
public start() {
// ... code shown above ...
this.app.listen(4000, '0.0.0.0', 511, function() {
console.log("HTTP server listening on http://localhost:4000/");
});
}
Finally, we add a helper method for rendering a simple HTML page so we can view our Todos and edit them:
export class WebUI {
// ... methods shown above ...
protected async renderHomePage(): string {
let tasks = await this.manager.getTasks();
let todos = tasks.filter((task) => task instanceof Todo);
return `
<html>
<body>
Your to-do's:
<ul>
${
todos.length === 0 ? '<li><em>none</em></li>\n'
: todos.map((todo) =>
'<li>'+todo.label+' <small>(id: '+ todo.id +')</small></li>'
).join('\n')
}
</ul>
<hr/>
<form action="/create" method="get">
<input type="hidden" name="type" value="Todo">
Label: <input type="text" name="label"><br>
<input type="submit" value="Create Todo">
</form>
<hr/>
<a href="/clear">[Clear All]</a>
</body>
</html>
`;
}
}
function redirect(res: Response, url: string) {
res.header('Cache-Control', 'no-cache');
res.redirect(301, url);
}
At this point, we have to launch our web server. For this purpose, we create a small launch script that
configures the dependency injection (as shown in the section on dependency injection, above), creates an
instance of class WebUI
, and invokes method start()
:
import { Storage } from "Storage";
import { StorageMongoDB } from "StorageMongoDB";
import { WebUI } from "WebUI";
import { N4Injector } from "n4js/lang/N4Injector";
@Binder
@Bind(Storage, StorageMongoDB)
class Binding { }
@GenerateInjector
@UseBinder(Binding)
class Root { }
N4Injector.of(Root).create(WebUI).start();
You can now use the HTML page by going to http://localhost:4000/, allowing you to interact with all of the logic we have built so far in order to read Todos, create new Todos and clear the storage.
All of the fundamental elements of our model are completed and we have a functioning Task Manager with a simple Web UI.
Export as npm
Finally, we can export our project as an npm package to integrate it into other Node.js projects or to launch from command line.
-
You can export one or multiple projects by selecting them in the Project Explorer and opening the "Export …" wizard by right-clicking on them. On the first page select "N4JS Exports / N4JS npm Export". For the purpose of this example, only export project
n4js.example.tasks
. -
On the second page you have to choose a target folder to export to.
Export to a folder outside your Eclipse workspace! |
-
By default, the exporter exports as a directory. Optionally, you can export as a versioned tarball by checking the option "Compress the contents of the file".
-
The last page of the wizard shows a preview of the package.json file that will be created in the exported npm package, together with a comparison to an existing package.json file in your N4JS project, if present. You can place such a package.json file next to your
manifest.n4mf
file (optional), in case you need to define more specifics in the package.json, that are not covered by the N4JS export wizard.
![npmexport](images/npmexport.png)
Run from command line with Node.js
Once you’ve exported your project, you can either publish it to npmjs.com or install directly from your hard disk and call the launch module, for example
$ npm install -g n4js.example.tasks
$ node -r n4js.example.tasks/launch
HTTP server listening on http://localhost:4000/
In case you want to install dependencies and run it right away in the exported npm folder, you have to
manually set up the NODE_PATH
to the folder hosting the exported npms, otherwise node cannot resolve the
project/npm IDs.
$ export NODE_PATH=`pwd`
$ pushd n4js.example.tasks; npm install; popd
n4js.example.tasks@0.0.1 /Users/me/prj/n4js.example.tasks
├─┬ express@4.13.4
...
$ node n4js.example.tasks/launch.js
HTML server listening on http://localhost:4000/
So far we’ve been loading all code via node’s require()
function.
In case you want to load your modules via SystemJS,
which has more support to resolve cyclic dependencies across modules, you could use a handy starter
executable called n4js
. The n4js
starter is part of the n4js-node
runtime environment npm (a default
dependency of any exported npm) and is therefore already installed:
$ export NODE_PATH=`pwd`
$ cd n4js.example.tasks; npm install
n4js.example.tasks@0.0.1 /Users/me/prj/n4js.example.tasks
├─┬ express@4.13.4
...
$ ./node_modules/.bin/n4js n4js.example.tasks/launch
HTTP server listening on http://localhost:4000/
Bibliography
bibliography::[]