Ace is Sinatra for Node; a simple web-server written in CoffeeScript with a straightforward API.
Every request is wrapped in a Node Fiber, allowing you to program in a synchronous manner without callbacks, but with all the advantages of an asynchronous web-server.
app.put '/posts/:id', ->
@post = Post.find([email protected]).wait()
@post.updateAttributes(
name: @params.name,
body: @params.body
).wait()
@redirect @post
Ace is built on top of the rock solid Strata HTTP framework.
##Examples
You can find an example blog app, including authentication and updating posts, in Ace's examples directory.
##Usage
Node >= v0.7.3 is required, as well as npm. Ace will run on older versions of Node, but will crash under heavy load due to a bug in V8 (now fixed).
To install, run:
npm install -g git://github.com/maccman/ace.git
To generate a new app, run:
ace new myapp
cd myapp
npm install .
To serve up an app, run:
ace
##Routing
In Ace, a route is a HTTP method paired with a URL matching pattern. For example:
app.get '/users', ->
'Hello World'
Anything returned from a routing callback is set as the response body.
You can also specify a routing pattern, which is available in the callback under the @route
object.
app.get '/users/:name', ->
"Hello #{@route.name}"
POST, PUT and DELETE callbacks are also available, using the post
, put
and del
methods respectively:
app.post '/users', ->
@user = User.create(
name: @params.name
).wait()
@redirect "/users/#{@user.id}"
app.put '/users/:id', ->
@user = User.find([email protected]).wait()
@user.updateAttributes(
name: @params.name
).wait()
@redirect "/users/#{@user.id}"
app.del '/user/:id', ->
@user = User.find([email protected]).wait()
@user.destroy().wait()
@redirect "/users"
##Parameters
URL encoded forms, multipart uploads and JSON parameters are available via the @params
object:
app.post '/posts', ->
@post = Post.create(
name: @params.name,
body: @params.body
).wait()
@redirect "/posts/#{@post.id}"
##Request
You can access request information using the @request
object.
app.get '/request', ->
result =
protocol: @request.protocol
method: @request.method
remoteAddr: @request.remoteAddr
pathInfo: @request.pathInfo
contentType: @request.contentType
xhr: @request.xhr
host: @request.host
@json result
For more information, see request.js.
You can access the full request body via @body
:
app.get '/body', ->
"You sent: #{@body}"
You can check to see what the request accepts in response:
app.get '/users', ->
@users = User.all().wait()
if @accepts('application/json')
@jsonp @users
else
@eco 'users/list'
You can also look at the request format (calculated from the URL's extension). This can often give a better indication of what clients are expecting in response.
app.get '/users', ->
@users = User.all().wait()
if @format is 'application/json'
@jsonp @users
else
@eco 'users/list'
Finally you can access the raw @env
object:
@env['Warning']
##Responses
As well as returning the response body as a string from the routing callback, you can set the response attributes directly:
app.get '/render', ->
@headers['Transfer-Encoding'] = 'chunked'
@contentType = 'text/html'
@status = 200
@body = 'my body'
You can set the @headers
, @status
and @body
attributes to alter the request's response.
If you only need to set the status code, you can just return it directly from the routing callback. The properties @ok
, @unauthorized
and @notFound
are aliased to their relevant status codes.
app.get '/render', ->
# ...
@ok
##Static
By default, if a folder called public
exists under the app root, its contents will be served up statically. You can configure the path of this folder like so:
app.set public: './public'
You can add static assets like stylesheets and images to the public
folder.
##Templates
Ace includes support for rendering CoffeeScript, Eco, EJS, Less, Mustache and Stylus templates. Simply install the relevant module and the templates will be available to render.
For example, install the eco module and the @eco
function will be available to you.
app.get '/users/:name', ->
@name = @route.name
@eco 'user/show'
The @eco
function takes a path of the Eco template. By default, this should be located under a folder named ./views
.
The template is rendered in the current context, so you can pass variables to them by setting them locally.
If a file exists under ./views/layout.*
, then it'll be used as the application's default layout. You can specify a different layout with the layout
option.
app.get '/users', ->
@users = User.all().wait()
@mustache 'user/list', layout: 'basic'
##JSON
You can serve up JSON and JSONP with the @json
and @jsonp
helpers respectively.
app.get '/users', ->
@json {status: 'ok'}
app.get '/users', ->
@users = User.all().wait()
@jsonp @users
By default @jsonp
uses the @params.callback
parameter as the name of its wrapper function.
##Fibers
Every request in Ace is wrapped in a Fiber. This means you can do away with the callback spaghetti that Node applications often descend it. Rather than use callbacks, you can simply pause the current fiber. When the callback returns, the fibers execution continues from where it left off.
In practice, Ace provides a couple of utility functions for pausing asynchronous functions. Ace adds a wait()
function to EventEmitter
. This transforms asynchronous calls on libraries such as Sequelize.
For example, save()
is usually an asynchronous call which requires a callback. Here we can just call save().wait()
and use a synchronous style.
app.get '/projects', ->
project = Project.build(
name: @params.name
)
project.save().wait()
@sleep(2000)
"Saved project: #{project.id}"
This fiber technique also means we can implement functionality like sleep()
in JavaScript, as in the previous example.
You can make an existing asynchronous function fiber enabled, by wrapping it with Function::wait()
.
syncExists = fs.exists.bind(fs).wait
if syncExists('./path/to/file')
@sendFile('./path/to/file)
Fibers are pooled, and by default there's a limit of 100 fibers in the pool. This means that you can serve up to 100 connections simultaneously. After the pool limit is reached, requests are queued. You can increase the pool size like so:
app.pool.size = 250
##Cookies & Session
Sessions are enabled by default in Ace. You can set and retrieve data stored in the session by using the @session
object:
app.get '/login', ->
user = User.find(email: @params.email).wait()
@session.user_id = user.id
@redirect '/'
You can retrieve cookies via the @cookie
object, and set them with @response.setCookie(name, value)
;
app.get '/login', ->
token = @cookies.rememberMe
# ...
##Filters
Ace supports 'before' filters, callbacks that are executed before route handlers.
app.before ->
# Before filter
By default before filters are always executed. You can specify conditions to limit that, such as routes.
app.before '/users*', ->
The previous filter will be executed before any routes matching /users*
are.
As well as a route, you can specify a object to match the request against:
app.before method: 'POST', ->
ensureLogin()
Finally you can specify a conditional function that'll be passed the request's env
, and should return a boolean indicating whether the filter should be executed or not.
app.before conditionFunction, ->
If a filter changes the response status to anything other than 200, then execution will halt.
app.before ->
if @request.method isnt 'GET' and [email protected]
@head 401
##Context
You can add extra properties to the routing callback context using context.include()
:
app.context.include
loggedIn: -> [email protected]_id
app.before '/admin*', ->
if @loggedIn()
@ok
else
@redirect '/login'
The context includes a few utilities methods by default:
@redirect(url)
@sendFile(path)
@head(status)
@basicAuth (user, pass) ->
user is 'foo' and pass is 'bar'
##Configuration
Ace includes some sensible default settings, but you can override them using @set
, passing in an object of names to values.
@app.set static: true # Serve up file statically from public
sessions: true # Enable sessions
port: 1982 # Server port number
bind: '0.0.0.0' # Bind to host
views: './views' # Path to 'views' dir
public: './public' # Path to 'public' dir
layout: 'layout' # Name of application's default layout
logging: true # Enable logging
Settings are available on the @settings
object:
if app.settings.logging is true
console.log('Logging is enabled')
##Middleware
Middleware sits on the request stack, and gets executed before any of the routes. Using middleware, you can alter the request object such as HTTP headers or the request's path.
Ace sits on top of Strata, so you can use any of the middleware that comes with the framework, or create your own.
For example, we can use Ace's methodOverride middleware, enabling us to override the request's HTTP method with a _method
parameter.
strata = require('ace').strata
app.use strata.methodOverride
This means we can use HTML forms to send requests other than GET
or POST
ones, keeping our application RESTful:
<form action="<%= @post.url() %>" method="post">
<input type="hidden" name="_method" value="delete">
<button>Delete</button>
</form>
For more information on creating your own middleware, see Strata's docs.
##Credits
Ace was built by Alex MacCaw and Michael Jackson.