ZondaJS is node.js web framework for building web apps.
For a better experience, read this doc at http://documentup.com/chuqui/zondajs
Zonda is a powerful, dry wind. ZondaJS starts as a project to support an startup I'm working. I needed a couple of features for it, such us:
- Small footprint. You know, if you are responsible for a project, you need to know every detail on what you are usign. Having a small footprint allows you to know whats happening without becoming crazy or spending hours or even days reading code.
- Customizable. I want to be able, without pain, to use the what I want to use and not what the framework makes me use.
- Skeleton oriented. I don't want to have 1 big .js file with all the controllers nor being requiring a lot of files.
- Component oriented. I really like having architecture. Having Controllers, Services, DTOs, Mappers, Models etc, etc.. will allow to find & fix issues and scale much faster.
- Simple to use. I wanted something declarative and well organized, without "magic" behind.
- API cappable. Support for building REST services without pain.
- Though for big projects. The way it works and the set of features makes it ideal for big projects.
- MVC.
- Dependency Injection.
- Component oriented.
- Skeleton oriented.
- Properties support.
- Customizable middleware.
- Serve static files.
- REST support.
- Cookies support, also with signed cookies.
- Dead-Simple
- Session middleware (using existing cookies support)
- Clean up and publish on NPM repo
- Important! REDO the dependency injection feature, as it doesn't provide a way to run on minified/uglyfied code.
Rewriting controllers and middleware architecture
Node versions
- node @ >=0.8.x
Dependencies
- underscore@1.5.2
- swig@~1.3.2
- formidable@1.0.14
- cookies@0.3.8
- keygrip@1.0.0
- mime@1.2.11
Test dependencies
- mocha@1.16.2
Say hello to our Hello World app example.
var app = require('zondajs');
app.controller.get('/', function(request, response){
response.end("Hello World");
});
app.startApp(8080);Ok, Ok, the hardcoded "Hello World" strings looks really bad. So..
var app = require('zondajs');
app.properties.set('greetings', "Hello World");
app.controller.get('/', function(request, response){
response.end(app.properties.get('greetings'));
});
app.startApp(8080);Or even imagine it comes from a Service (using dependency injection):
var app = require('zondajs');
app.component('messageService',
{
getGreeting : function(){
return "Hello World":
}
}
);
app.controller.get('/', function(request, response, messageService){
response.end(messageService.getGreeting());
});
app.startApp(8080);Not available right now. It'll be available on the npm repo soon... ;)
Just run the zondajs command and it will drive you to the creation process.
$ zondajsThe command will ask you some basic data to build a default package.json file. It will ask:
- Project name
- Project description
- Author
After the project is created, you can start it by running
$ node app.jsAnd open a browser at http://localhost:8080
The following happens in ZondaJs when a request get to the server:
- The raw Node request and response objects are enhanced.
- The URL is parsed with the node url module.
- Zonda looks for a controller, based on the requested URL and method.
- The components are injected recursively, if any.
- The route named params are extracted and added to the request.params object.
- The query string params are added to the request.params object.
- All the middleware are ran. Without any default order.
- The controller is ran.
- The response is sent. Once the first step is completed, any other step has access to the rendering, in order to send errors, redirections, files, etc.
By Wikipedia: Dependency injection is a software design pattern that allows the removal of hard-coded dependencies and makes it possible to change them, whether at run-time or compile-time.
In ZondaJS, dependency injection is implemented by a key-object set, and triggered by the dispatcher when a controller that matches the requested URL is found.
You can set a new candidate component for dependency injection by usign the ZondaJS's component object.
This way, you don't need to rewrite or repeat code. You only need to write it once as component and then, inject it whereever you need it.
Middleware are functions that get ran for every request, before the controller method is called.
Middleware is really usefull to prepare the request and response objects for your need.
As you have access to the full request and response objects, you can also render, redirect, validate, write a file, return an error page and any other task you could imagine doing.
Routing in ZondaJS is pretty simple. It supports the GET, POST, PUT and DELETE HTTP methods.
You can match the exact url by typing it in your controller or use wildcards with the :name format.
app.controller.get('/', function(request, response){response.end();});
// matches url / and only for GET requests
app.controller.post('/', function(request, response){response.end();});
// matches the / url only for POST requests
app.controller.get('/:all', function(request, response){response.end();});
// matches:
// /product
// /product/12
// /product/shoes/the-shoes-i-like
app.controller.get('/product/:id', function(request, response){response.end();});
// matches:
// /product/12
// /product/shoes/the-shoes-i-like
// it does not match:
// /product
// /product/
// /anyother
app.controller.get('/product/:category/:id', function(request, response){response.end();});
// matches
// /product/shoes/12
// /product/beers/frozen
// doesn't match
// /product/beer
// /productAnd so on. An important feature is that you can access that named value from your request.
app.controller.get('/product/:category/:id', function(request, response){
console.log(request.params.category);
console.log(request.params.id)
response.end();
});By default, ZondaJS uses SWIG for rendering. Read more on the SWIG templating engine on http://paularmstrong.github.io/swig/
You can render at any point of the execution, actually after the step 1 (See execution flow), and there are a couple of handy methods:
var data = {
title: 'Rendering',
animals: [
"Dog",
"Cat",
"Monkey"
]
};
response.render('mytemplate', data);
// renders the data object into the provided template
response.sendError(404);
// renders the 404 custom error page
response.sendJSON(data);
// returns the data serialized with JSON format and content-type.Modifying the default rendering engine.
Anyways, you can override the zondajs.enhancements.response object to use the rendering engine you like. This would be like:
app.enhancements.response.render = function(response){
return function(template, data){
// Do your rendering stuff
};
};
app.enhancements.response.sendError = function(response){
return function(code){
//do your rendering stuff here
//in this case, we need to fetch the templates from /views/errors/
};
};ZondaJS comes with a load function that enables you yo load dinamically your project files.
The loader is not actually a loader but a file indexer, so when you load a folder, it will call you callback function giving it the complete relative path to the .js files in it (require ready path). Then you can decide if require them or not.
app.loader.load('./controllers', function(name, filepath){
console.log(name + ' controller about to be loaded');
require(filepath);
console.log(name + ' loaded');
});ZondaJS supports Custom Error pages. When you create a ZondaJS project you will find a views/errors folder in your project path. Error pages are plain HTML pages, with no binding or templating. This ensures you can send an error and render the error page from any point within your application.
By default, we include 3 (really simple) error pages.
- 400.html
- 404.html
- 500.html
Of course, you can add more pages and customize their contents in order to make them match your branding/ideas.
We highly recommend to serve static files from CDN or having apache, nginx, cherokee or the server you like for doing this tasks.
Anyways, in order to let you run faster, we included a middleware called "static" which serve the files placed on the static folder.
I really encourage you to read the code, it has comments for every object created. You will end up with a much better understanding of the framework than by looking at the provided functions.
Conventions for this docs
The following function 'functionName' return undefined and takes parameter1 and parameter2 as it's parameters
functionName(parameter1, parameter2)
The following function 'functionName' return an object and takes parameter1 as it's parameters
object: functionName(parameter1)
app.component(componentName, componentImplementation) Where:
- componentName is the name or key used to inject the component.
- componentImplementation is the object to be returned. See below more on functions.
// lets start with a really simple component called "simpleComponent"
app.component('simpleComponent', {
greeting : "Hello world!"
});
// lets make this component a bit smarter
app.component('simpleComponentSmarter', {
greeting : "Hello world!"
sayHello: function(){
console.log(this.greeting);
}
});
/**
* We have been creating components by declaring them as objects, but there is a
* really powerfull feature here.
*
* We can inject an existing component to another component!
* In that case, we need to use a function which returns the actual object
*/
// We set an existing component name as parameter
app.component('greetingMessage', function(simpleComponent){
return {
sayHello: function(){
// then we can just use it
console.log(simpleComponent.greeting);
}
};
});So, Why we need to return an object?
When the dispatcher start injecting the dependencies, it will run the component by providing it's parameters and use the result to be injected as object. Then, ZondaJS does not inject functions, but only inject objects.
app.properties.set(key, value) Where:
- key is the key used to identify the property value.
- value is the object returned when the property key is fetched.
// set a string
app.properties.set('greetings', "Hello World");
// set an object
app.properties.set('config', {
port: 8080,
version: '@1.0.1'
});
//since functions are objects...
app.properties.set('myWeirdFunction', function(){
console.log('I should be a bad practice, but I am still possible');
});object : app.properties.get(key, value) Where:
- key is the key of the property you want to get.
If no property is found for the given key, undefined will be returned instead.
app.properties.get('greetings'); // "Hello World"
app.properties.get('myWeirdFunction')(); // outputs 'I should be a bad practice, but I am still possible'
app.properties.get('typoedName'); // undefinedapp.loader.load(path, callback) Where:
-
path is the relative route to the folder you want to index.
-
callback is function that receives 2 params.
-
callback(name, relativePath)
-
name is the file name (e.g. cookies)
-
relativePath is the require ready path to the file (e.g. './components/cookies.js')
As seen before, the loader is not actually a loader but a file indexer, so when you load a folder, it will call you callback function giving it the complete relative path to the .js files in it (require ready path). Then you can decide if require them or not.
app.loader.load('./controllers', function(name, filepath){
console.log(name + ' controller about to be loaded');
require(filepath);
console.log(name + ' loaded');
});
// another kind of loader to save everything into an object
var myStuff = {};
app.loader.load('./myStuff', function(name, filepath){
console.log(name + ' controller about to be loaded');
myStuff[name] = require(filepath);
console.log(name + ' loaded');
});
// then you can access it by doing something like:
myStuff.myModule.myFunction();The app.controllers object let you add controllers based on the method you want to map. All the functions in the mentioned object receive 2 parameters, the path and the controller implementation.
Basically, they add the controller to the routes array with the data required to be mapped and ran when a server request match the route.
A HTTP GET controller
app.controller.get(path, controllerImplementation) Where:
- path is the controller path where the controller implementation will be mapped to.
- controllerImplementation is function that receives at least 2 params, request and response. If it receives more parameters, they will be injected based on the parameter name
app.controller.get('/', function(request, response){
response.end('GET /');
});A HTTP POST controller
app.controller.post(path, controllerImplementation) Where:
- path is the controller path where the controller implementation will be mapped to.
- controllerImplementation is function that receives at least 2 params, request and response. If it receives more parameters, they will be injected based on the parameter name
app.controller.post('/', function(request, response){
response.end('POST /');
});A HTTP PUT controller
app.controller.put(path, controllerImplementation) Where:
- path is the controller path where the controller implementation will be mapped to.
- controllerImplementation is function that receives at least 2 params, request and response. If it receives more parameters, they will be injected based on the parameter name
app.controller.put('/', function(request, response){
response.end('PUT /');
});A HTTP DELETE controller
app.controller.del(path, controllerImplementation) Where:
- path is the controller path where the controller implementation will be mapped to.
- controllerImplementation is function that receives at least 2 params, request and response. If it receives more parameters, they will be injected based on the parameter name
app.controller.del('/', function(request, response){
response.end('DELETE /');
});ZondaJS uses SWIG for views. Check the SWIG site for more info. See http://paularmstrong.github.io/swig/
You can always modify it to use the rendering engine you like. See Rendering section above.
Middlewares are functions that are going to be ran for every request.
Middleware have access to the request and response object, both for reading and writing purpose.
When a middleware is done doing its activity, it must call a callback function, passing the request and response objects.
Adds a middleware
app.middleware.use(middlewareFunction) Where:
- middlewareFunction is the function that implement the middleware functionality.
Also, middlewareFunction is expected to be like:
function(request, response, next) Where:
- request is current request object.
- response is current response object.
- next is the callback function to be called when the middleware has ended doing it's tasks. It receives the request and response objects.
// Dummy middleware
app.middleware.use(function(request, response, next){
if(request.method === 'GET'){
console.log('got a get baby');
}
next(request, response);
});
// add cookies support using the cookies and keygrip libraries
var cookies = require('cookies');
var keygrip = require('keygrip');
// change these passwords
keys = keygrip(['&083#$%^df8', 'Nk]L2E3Lmk', '65[5AyE3%'], 'sha256', 'hex');
var cookiesMiddleware = function(request, response, next){
request.cookies = cookies(request, response, keys);
next(request, response);
};
app.middleware.use(cookiesMiddleware);ZondaJS uses the same HTTP server module node provides.
It adds some handy methods, know internally as enhancements, to facilitate the developer tasks.
Rendering a view
response.render(template, dataObject) Where:
- templateName is the template file to render to.
- dataObject is the object containing the information (property-value) to be rendered.
app.controller.get('/', function(request, response){
response.render('home.html', {
title: 'Home page',
message: 'Welcome to my home page!'
});
});Rendering JSON
response.sendJSON(dataObject) Where:
- dataObject is the object containing the information (property-value) to be serialized and rendered.
app.controller.get('/json/test', function(request, response){
response.sendJSON({
id: 1242,
name: 'Foo bar'
categories: [
'javascript',
'css',
'html'
]
});
});Rendering a custom error page
response.sendError(errorCode) Where:
- errorCode is HTTP error code. Notice also that a HTML file must exist in the views/errors/ folder with the errorCode as file name (e.g. 404.html)
app.controller.get('/testing404', function(request, response){
response.sendError(404);
});Answering the request with a file.
response.sendFile(filePath) Where:
- filePath is the path to the file to be returned.
app.controller.get('/get/report', function(request, response){
response.sendFile('docs/report.pdf');
});Sending a redirection:
response.redirect(path) Where:
- path is the path/URL to be redirected to.
app.controller.get('/logout', function(request, response){
response.redirect('/');
});Same as the response object. The request object comes from Node's HTTP module. Here are some enhancements on it.
It exposes a params object containingd the parameters (from named params, querystring and post methods)
request.params
app.controller.get('/product/:id', function(request, response){
console.log(request.params);
console.log(request.params.id);
response.end('Done');
});If you are expecting a request body, it is available by:
request.body
app.controller.put('/api/product/:id', function(request, response){
console.log(request.params);
console.log(request.params.id);
updatingToObj = JSON.parse(request.body);
response.end('Done');
});If you are doing file uploads, it exposes an array with all the files:
request.files
app.controller.post('/api/product/:id', function(request, response){
console.log(request.params);
console.log(request.params.id);
console.log('And the uploaded fiels are:');
console.log(request.files);
response.end('Done');
});A cookies object is always exposed (when using the default cookies middleware):
request.cookies
app.controller.get('/cookietest', function(request, response){
request.cookies.set( "unsigned", "foo", { httpOnly: false } );
request.cookies.set( "signed", "bar", { signed: true } );
response.redirect('/cookietestcheck');
});
app.controller.get('/cookietestcheck', function(request, response){
var unsigned = request.cookies.get( "unsigned" ); // foo
var signed = request.cookies.get( "signed", { signed: true } ); // bar
response.end('Done');
});If you check the ZondaJS source code, you will find 2 more objects:
- __routes is the route handler.
- __di is the dependency injection manager.
These objects are meant to be private. Don't modify them unless you completely know what you are doing.
Finally, but not least, ZondaJS exposes the most important function.
Launching the app.
app.startApp(port) Where:
- port is the port the server will listen for requests.
app.startApp(8080);Any kind of help/feedback is highly appreciated!
If you want to contribute by coding ZondaJS, please follow this steps:
- Fork
- Create your feature branch
- Send a pull request
Great! In this case you don't need to send a pull request. Just package your code and tests, and provide documentation on how to install and use it. This code will be 100% yours and you will be the one maintaining and publishing it to npm. Of course, we are interested on supporting and helping you.
Naming convention
- zondajs-<your_project_name>-<type>
Where
- your_project_name is, well, you already know!, your project name.
- type is one of "component", "middleware", &c.
So, the idea is to keep names simple and self-explanatory. Example names are:
- zondajs-dbsession-middleware
- zondajs-validation-component
- zondajs-oAuth-component
- and so on...
Just create an issue, so we can discuss about it.
Let me know if you are using ZondaJS. I wanna know what's your opinion and how can we improve the framework!.
Check the ZondaJS's github issues page
This project is licensed under the MIT license. Read the LICENSE file for more info on it.
- Logo and favicon.ico By: Icons Land - http://www.icons-land.com

