Nodal follows MVC patterns you're probably familiar with from the simplicity of Django and Rails. If you need more in-depth help, full API documentation is available here. Otherwise, let's get started!
To begin your project, first install the latest version of Node (4.x) from nodejs.org. Once you've completed that, open your terminal and run:
npm install nodal -g
(If you get an error, run sudo npm install nodal -g
or fix permissions permanently by following these directions)
It will take a few seconds to finish. At this point, you have the Nodal
command line tools available and you can really get started!
Next, run:
nodal new
A few prompts will walk you through the project creation process. Once you're done, visit your new project folder.
Begin your Nodal server with:
nodal s
And voila! You'll notice the port that it's running on is 3000
. If you want to
change that, go to config/secrets.json
:
{
"development": {
"port": 3000,
"auth": {
"key": ""
}
},
"production": {
"port": "{{= env.PORT }}",
"auth": {
"key": ""
}
}
}
And modify the "port": 3000,
line to whatever you'd like. Wait - is this magic?
Not really. Every file in ./config/ will get loaded into a special Config object.
Namely, into Nodal.my.Config.{filename}
assuming const Nodal = require('nodal')
.
Remember this! This is very important for setting environment variables. The
values that are loaded are based on your environment, set by your NODE_ENV
environment
variable (defaults to development).
Let's see why the server is running on port 3000 by looking in ./server.js
...
module.exports = (function() {
'use strict';
const Nodal = require('nodal');
let daemon = new Nodal.Daemon('./app/app.js');
daemon.start(function(app) {
app.listen(Nodal.my.Config.secrets.port);
});
})();
Not so magic after all. We're telling the server daemon to start, and once it's running,
to tell your app to listen on the provided port. You'll notice the Nodal.Daemon
takes
one argument. That's your application that it's going to load and instantiate.
Your server has started, but how do HTTP requests know where to go? Easy,
let's look in ./app/router.js
.
module.exports = (function() {
'use strict';
const Nodal = require('nodal');
const router = new Nodal.Router();
const IndexController = Nodal.require('app/controllers/index_controller.js');
const StaticController = Nodal.require('app/controllers/static_controller.js');
const Error404Controller = Nodal.require('app/controllers/error/404_controller.js');
/* generator: begin imports */
/* generator: end imports */
router.route('/', IndexController);
router.route('/static/*', StaticController);
/* generator: begin routes */
/* generator: end routes */
router.route(/.*/, Error404Controller);
return router;
})();
Things to make note of here: The design pattern for module.exports = (function() { /*...*/ })()
where you export an IIFE is the standard in Nodal. The reason we do this is to encourage
consistency in understanding what's getting exported. A developer looking to quickly debug
can jump to the bottom of a file and look for the return statement to see what was
actually exported.
Next, Controllers are all loaded very explicitly. This follows Django's routing style. The router itself takes regex and will compare an incoming request to its pattern in the order that they're added.
Also make note of the /* generator: ... */
lines. These are used as boilerplate
for automatic controller importing / route addition from the command line file generators.
You should avoid removing these lines.
We see in the basic routing structure that we check to hit the index and the static folder before going through generated routes (special cases) and finally fall back to a 404 if nothing is hit.
So what exactly happens when you hit the index route? Exploring ./app/controllers/index_controller.js
gives us the answer.
module.exports = (function() {
'use strict';
const Nodal = require('nodal');
class IndexController extends Nodal.Controller {
get() {
this.render(
this.app.template('index.html').generate(
this.params,
{
name: 'My Nodal Application'
}
)
);
}
}
return IndexController;
})();
When the index route is hit, a new instance of IndexController
is created.
This is given some special properties accessible from referring to the instance.
Within a method, this.params
contains client request parameters (this.params.query
,
this.params.id
, this.params.body
, this.params.ip_address
, etc.) and
this.app
refers to your main Application
instance, incase you need
to access any "global" properties from it.
We can see here a get()
method, which is called when an HTTP GET
request
comes to the controller. Anything not specified will fall back to a
501 - Not Implemented
response. Available methods that can be triggered from
a route are:
get()
post()
put()
del()
options()
Or, some CRUD-like equivalents:
index() // GET with no this.params.id
show() // GET with a this.params.id
create() // POST
update() // PUT
destroy() // DELETE
To render a string from within a controller, use this.render()
. It can also
render JSON automatically if you provide a serializable object. For API-formatted
responses use: this.respond()
. Other ways to respond to requests are
available in the API documentation.
Please keep in mind that ES6 arrow functions (fnAdd = (a, b) => a + b;
)
do not create a new context for this
. If you have nested callbacks
in a Controller (quite common), it is a best practice to keep using anonymous
arrow functions to preserve your reference to the controller instance so responses
are easy to send out. (No self = this;
anti-patterns.)
Create a new controller with the CLI using
nodal g:controller ControllerName
It will then create controller_name_controller.js
in the base ./controllers/
directory.
If you'd like to put it in another directory, use:
nodal g:controller path/to/ControllerName
Generating controllers this way will also automatically create best-guess routes in router.js.
You can create a controller for a specific model (auto include CRUD features) with:
nodal g:controller --for:ModelName
Creating models is easy. Your project won't start out with any (they're not necessary for all server types), but you can generate them with:
nodal g:model ModelName
This will create both a Model and a Migration. The Model will look like;
module.exports = (function() {
'use strict';
const Nodal = require('nodal');
class ModelName extends Nodal.Model {}
ModelName.setDatabase(Nodal.require('db/main.js'));
ModelName.setSchema(Nodal.my.Schema.models.ModelName);
return ModelName;
})();
Our database is set from ./db/main.js
which grabs connection data from
./config/db.json
. (Explore both files to see what's happening. Same as
./config/secrets.json
above.) Note that Nodal currently only supports PostgreSQL.
Our schema is set from Nodal.my.Schema
which automatically loaded ./db/schema.json
upon starting the server. (For this reason, Schema changes require app shutdowns and reloads.)
Nodal comes with two "special", pre-built models. User
and AccessToken
.
Create these with:
nodal g:model --user
nodal g:model --access_token
These models just have some additional libraries / functionality included that make things like passwords and OAuth a little bit easier. Part 2 of the Introduction screencast begins to cover this.
With your Model, a Migration was created in ./db/migrations
. In order to load
your Model data into a schema and create the necessary database connections,
make sure PostgreSQL is running and make sure your database is created:
nodal db:create
Once your database is created, prepare it for migrations (drops schema, clears table)
nodal db:prepare
And finally, run all pending migrations with:
nodal db:migrate
If you need to undo a migration simply run (one migration at a time):
nodal db:rollback
If you'd like to do a stepwise migrations or rollbacks, use the flag:
nodal db:migrate --step:1
To use your Model in a controller, import it with:
const Model = Nodal.require('app/models/my_model.js');
Nodal.require
is just require
pointing to your main app directory.
You can do fun things with your Model like;
let myModel = new MyModel({field1: 'a', field2: 'b'}); // create a Model, not in db
myModel.save(err, callback); // Save model instance to db
myModel.destroy(err, callback); // Remove model instance from db
Model.create(params, callback); // creates and saves your model to db right away
Model.find(id, callback); // find a Model with a specified id
Model.update(id, params, callback); // update a Model with a specified id
Model.destroy(id, callback); // ... etc.
/* And queries! */
let query = Model.query(); // Instantiate Composer (ORM instance)
// method chaining :)
query
.where({id__gt: 7})
.orderBy('id', 'DESC')
.limit(5)
.where({field1__like: 'lol'})
.end((err, myModels) => {
// do something with my models
});
Nodal has basic support for the Postgres JSONB data type. Any model that has a json column that contains a valid JSON object will have the object automatically marshalled in and out of the database. Let's walk through how to setup and use a model with a json column.
First, lets generate a new model
nodal g:model Recipe name:string, flags:json
Open the migration that was generated and you will see the following in the up()
method.
this.createTable("recipes", [{"name":"name","type":"string,"},{"name":"flags","type":"json"}])
Let's modify the up command to add an index on the json column. Your up()
method should look something like this
up() {
return [
this.createTable("recipes", [{"name":"name","type":"string,"},{"name":"flags","type":"json"}])
this.createIndex("recipes", "flags", "gin")
];
}
Nodal provides two comparator's for querying the json data for matches or existence. Lets assume our Recipe table contains a collection of recipes, and the flags json column contains flags related to dietary restrictions like vegetarian, vegan or paleo. Lets query for recipes that are Paleo only
Recipe.query()
.where({flags__json:{ paleo: true }})
.end( (err,recipes) => {
......
});
The __json
comparator also allows for multi key queries:
Recipe.query()
.where({flags__json:{ vegan: true, paleo: true }})
.end( (err,recipes) => {
......
});
What if you want to query for all recipes that contain a certain key.
Recipe.query()
.where({flags__jsoncontains: 'paleo'})
.end( (err,recipes) => {
......
});
There's still a bunch more to cover! Information on in-depth Migrations, Initializers, Middleware, Schedulers, Tasks and Workers is coming soon.
Please watch the repository, keithwhor/nodal for updates.
Keep up with screencasts and more details on nodaljs.com.
Follow me on Twitter, @keithwhor