Kolba
A framework that shares your beliefs
Kolba is simple
var Kolba = ;var app = ; app; app;
Kolba is a simple web framework for nodeJS, heavily influenced by flask. Aims to provide the structure lacking in frameworks like express without the opinions baked in libraries like Rendr.
Installation
- Install:
npm install kolba
- And use:
% node
> var Kolba = require('kolba')
undefined
Big fat disclaimer
This is experimental, Kolba is not production ready.
Kolba uses some Node features that are not documented and may not be supported any more in the future. Play around with it, but do not even think about using it for anything real.
Defining resources
Resources consist of:
- A regular expression
- A callback
- A list of methods (optional)
- A string defining the returned content type (optional)
This is how you would define a resource:
var Kolba = ;var app = ; app; app;
The path
The syntax for the regexp used to match the path is defined by XRegExp, an amazing library created by Steven Levithan.
Thanks to XRegExp we can name groups and use them as callback parameters:
app;
The callback
Callbacks use a simplified version of AngularJS' dependency injection. Any of your callbacks can use the following objects just by declaring it in the signature:
request
: A Request object.response
: A Response object.locals
: A RequestLocals object.
Methods
A list of the methods your resource understands. Should be any of:
- DELETE
- GET
- HEAD
- OPTIONS
- POST
- PUT
You don't have to implement the HEAD
and OPTIONS
methods, they will be
appended to the list and managed automatically by Kolba.
Defaults to ['GET', 'HEAD', 'OPTIONS']
.
Content Type
The last parameter is a string defining the content type that your resource returns. This is used to find a suitable callback for a given request. You can avoid setting this explicitly if you define it in Kolba's constructor:
var Kolba = ;var app = 'contentType': 'text/plain'; app;
In this example every resource that doesn't specify explicitly a content type
will be treated as text/plain
.
Defaults to text/html
.
Static resources
To serve static files from a directory there is a special type of resource that receives a mount point and a directory:
app;
It will serve only GET
requests, and content-types will be guess using
node-mime.
% curl -v "http://127.0.0.1:3000/js/kolba.js"
* About to connect() to 127.0.0.1 port 3000 (#0)
* Trying 127.0.0.1... connected
> GET /js/kolba.js HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: 127.0.0.1:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Kolba
< Content-Type: application/javascript
(...)
This is available for development purposes, but it should not be used in production. Use a specific web server like apache or nginx to serve your static files, not your application server.
This is a very naive implementation, it is not safe to use live.
Accept-header-based resources
I need a shorter name for this, but the concept is very simple; as was mentioned before, a resource consists of:
- A URL
- A callback
- A list of methods
- The content type it returns
An example is worth a thousand words:
var Kolba = ;var app = ; app; app; app;
Note that both resources are mounted under the same path and respond to the same methods. Let's issue a request:
% curl -v http://127.0.0.1:3000/
* About to connect() to 127.0.0.1 port 3000 (#0)
* Trying 127.0.0.1... connected
> GET / HTTP/1.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Kolba
< Content-Type: text/plain; charset=utf-8
<
Hello Kolba!
Now let's add the Accept: application/json
header to tell the server that we
want a JSON response this time:
% curl -v http://127.0.0.1:3000/ -H "Accept: application/json"
* About to connect() to 127.0.0.1 port 3000 (#0)
* Trying 127.0.0.1... connected
> GET / HTTP/1.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Kolba
< Content-Type: application/json
<
{"Hello": "Kolba!"}
When Kolba receives a request, reads the Accept header sent by the browser and picks the callback that understands that format.
So what?
This way of defining resources allows you to have one URL return different formats. When you request a resource using a web browser, you will get an HTML representation of the content, but when your single page application (SPA) kicks in, it will request the information in JSON format.
Some examples:
app; app; app;
That way your pages will be rendered in the server on the first hit, and the consecutive renders will be done in the client.
You can (and should) abstract the common logic away. The JSON object generated by the last resource, for example, could be fed into any of the HTML resources to generate their templates.
Return codes
This table shows clearly which status code will be returned when looking up the correct resource for a given request:
+--------+--------+--------+--------+
| Path | Method | Accept | Code |
+--------+--------+--------+--------+
| false | false | false | 404 |
| false | false | true | 404 |
| false | true | false | 404 |
| false | true | true | 404 |
| true | false | false | 405 |
| true | false | true | 405 |
| true | true | false | 406 |
| true | true | true | 200 |
+--------+--------+--------+--------+
Middlewares
You can control when you middlewares get executed: as soon as a request is received or right before it gets sent to the client.
app; app; app;
Request middlewares
You can define request middlewares with the app.beforeRequest()
method. When
a request comes, this is the first thing that gets executed.
Every middleware is executed asynchronously, but not in parallel. Only when one
has finished, the next one starts. But if one returns a value different from
undefined
the execution is aborted and whatever was returned gets sent to the
client:
app; app;
Response middlewares
The app.afterRequest()
method defines a response middleware:
app;
The response middlewares get called right before the response is sent to the client. This lets you apply any transformation on the response, like this example shows, which gzips the body.
Unlike the request middlewares, when a response middleware returns a value, the execution won't be interrupted. Instead, the response will be updated with that value and the next middleware will be called. This will happen asynchronously, but it is also sequential.
Interceptors
It is possible to intercept a response based on its status code:
app;
Note: Be careful when modifying the body, make sure you are not breaking the format that the resource returned.
This allows you to define handlers for errors in a consistent way:
Note: You can only set 1 interceptor per status code.
app; app;
Interceptors are called right before the response middlewares.
Post mortems
If you want to execute a maintenance task after the response was sent to the
client, i.e. logging, use a post mortem
:
app;
How Kolba treats return values
Kolba embraces the dynamic nature of JavaScript and uses it to provide some handy flexibility. There are 4 types of values your resources can return, which will be treated differently.
String
Will be treated as the body of the response.
app;
Integer
Will be treated as the resource's status code.
app;
Response object
Will be used as is.
app;
Promise object
Must resolve to any of the 3 types above.
var Promise = Promise; app;
The Promise
object is a thin wrapper around cujojs'
when. You can use any promise library
actually, as long as it complies with the
Promises/A+ spec.
Any other object that doesn't match any of these will be simply serialized and sent to the client.
How Kolba treats Response objects
In every case, even when you return a Response
object, the contents of it
will be used to update the original response, it won't overwrite it. Feel free
to instantiate a new Response
, but keep in mind that when you return it, the
values that you didn't set explicitly won't be empty.
Say that you want to overwrite the X-Powered-By
header:
var Response = Response; app;
This won't remove it, because you didn't set a different one. If Kolba took the response as is, you would need to redefine every single attribute manually.
var Response = Response; app;
Request
The Request
object will contain information about the request in progress.
Specifically, it contains the following public attributes:
- path
- method
And the following public methods:
- getHttpRequest
- get
- acceptedTypes
- getHeader
You can access the Request
object from anywhere in your application simply by
running:
var request = ;
Read more about this in The magic behind getCurrentRequest().
httpRequest
This is the Request
object generated by Node. Whenever you need an attribute
that it's not directly provided by Kolba's Request
object, call the
getHttpRequest()
method or get('attributeName')
. This will get that
attribute from the original request object.
Response
This object is just a representation of the response that will be sent to the
client. Unlike Node's Response
object, this is not responsible for sending
the actual response to the client.
That will be done by the RequestLocals
object.
Events
Thanks to node's Domains, you can create your own EventEmitter object which will be automatically binded to the current request. That way you don't have to worry about concurrency.
Usually when you have 2 (or more) concurrent requests being processed by the same thread you have to be very careful that the events emitted in one of them don't pollute the others. As you may already know, Node is single threaded, which means that you have to handle this cases yourself. Kolba takes care of that for you.
The magic behind getCurrentRequest()
You may have noticed the following line already:
var request = ;
This allows you to access the request from any point in your application. Here I will explain how it works.
In frameworks like flask you can access the request information simply by importing it:
from flask import request
That is possible because flask uses thread locals (provided by Werkzeug's Context Locals) which is, basically, variables local to the current thread (but your application will see them as globals). That's how flask knows which of the current requests being processed you are asking for.
Node runs in a single thread, every incoming request will be served by the same thread, so this is not possible. However, Node provides a mechanism to mimic this behaviour, called domains.
From Node's documentation:
Domains provide a way to handle multiple different IO operations as a single group. If any of the event emitters or callbacks registered to a domain emit an error event, or throw an error, then the domain object will be notified, rather than losing the context of the error in the process.on('uncaughtException') handler, or causing the program to exit immediately with an error code.
In short, domains are a way to handle errors (and events in general) in a simpler way. As a side effect, you can also store information which will be globally accessible to the current domain. Your application will be sandboxed, which allows Kolba to simulate a thread locals behaviour.
This is very useful not only because prevents different requests from polluting each other (overwriting each other's variables) but also provides a mechanism for intra-request communication, i.e. triggering events that will be only delivered to listeners inside that same request.
Cons
The problem with this approach is that if you use it, your application will be relying on a global variable to function, and that can be bad.
You don't have to use it, though. You can ask for the request on your resource and pass it around.
app;
It's up to you.
With great power...
Don't inject information in the request object. Don't treat it as a global variable for you to store information. Treat it as immutable, read-only.
Remember that thanks to V8 optimizations reusing objects is very efficient, as long as you don't transform them at runtime. Kolba doesn't currently support sessions, which would be the way for you to persist the state of your application between requests. Use cookies for that.
Read more about Node's event loop and V8's memory management if you are interested in this subject.
Wish list
- Modularized applications
- The end of hunger in the world
- A better slogan
- Benchmarks
- Sessions