The lightweight web framework

Do you like Sinatra, but find it a little heavy? Bloated with things like simple error handling and CSRF protection; sick of the impedence mismatch of using one language on the server and another on the client; it’s not even asynchronous?!

Berliner is the framework for you! In just 16 lines of delicious CoffeeScript, it gives you all this, using the infinitely scalable horse power of Node.js:

Install it

$ npm install berliner

Hello, world

app = require 'berliner'

app.get '/', -> 'Hello, world!' 4567


app.public = __dirname + '/public' # where to find static files
app.views  = __dirname + '/views'  # where to find view templates
app.session_secret = 'abcde12345'  # key for encrypting sessions

Handling HTTP methods, statuses, headers, etc.

app.get '/confs/:name', ->
  JSON.stringify @params

app.put '/confs/:name', (name) ->

app.get '/download/*.*', ->
  @params.splat.join ', ' '/confs', ->
  @status 201
  @headers 'Content-Type': 'application/json'
  JSON.stringify @params

app.get '/legacy', ->
  @redirect '/hello'

app.options '/', ->
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, PUT, DELETE'
  @render ''

This app responds like so:

$ curl 'localhost:4567/confs/eurucamp?bears=awesome'

$ curl -X PUT localhost:4567/confs/jsconf

$ curl localhost:4567/download/foo.js
foo, js

$ curl -iX POST localhost:4567/confs -d 'horses=fake'
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 17
Set-Cookie: session=XABfKjq2xvCavSitaxu0BC9XSl...; Path=/; HttpOnly
Connection: keep-alive


$ curl -i localhost:4567/legacy
HTTP/1.1 303 See Other
Location: /hello
Connection: keep-alive
Transfer-Encoding: chunked

$ curl -iX OPTIONS localhost:4567/
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, DELETE
Connection: keep-alive

WebSocket and EventSource

app.websocket '/ws/:name', ->
  @socket.onmessage = (e)=>
    @socket.send + ': ' +

app.eventsource '/ws/:name', ->
  setInterval (=> @socket.send + ': PUSH!'), 5000


app.get '/counter', ->
  @session.counter ||= 0
  @session.counter += 1
  @render @session.counter.toString()


app.get '/', ->
  @cookie my_cookie: 'hello'
  @cookie another_cookie: {value: 'something', path: '/welcome', expires: new Date(2012,11,25), http: true}

app.get '/welcome', ->
  @render @cookies.my_cookie

Helpers, EJS and HAML

app.views = __dirname + '/views'

  site_name: -> ''

app.get '/hello', ->
  @ejs 'hello', locals: {name:}
# views/hello.ejs

Hello <%= name %>, welcome to <%= site_name() %>!

You can use @ejs or @haml to render a template. @render just takes a string and writes it to the response body. If your route handler returns a string, that string will be the body. Otherwise, you must call a rendering function.

$ curl 'localhost:4567/hello?name=_why'
Hello _why, welcome to!

You can also register templates in the app code itself:

app.template 'hello.ejs', """
Hello <%= name %>, welcome to <%= site_name() %>!

Context groups

You can set up a set of routes with a common prefix and add before-filters that only apply to that context.

app.get '/', -> 'Hello!'

app.context '/auth', (auth) ->
  auth.before (next) ->
    if @request.headers.authorization
      @status 401
      @render 'Authorization required'
  auth.get '/', -> 'Secret'
$ curl localhost:4567/
$ curl localhost:4567/auth
Authorization required
$ curl localhost:4567/auth -H 'Authorization: foo'


Created by James Coglan at Eurucamp 2012, inspired by Konstantin Haase. Released under the MIT license.

If you find bugs, you’re probably holding it wrong. Since this is such a small module, it’s almost certainly bug-free; in fact I didn’t bother writing tests because it’s obviously correct and test frameworks are ugly and stupid.