In this document we walk through the features of Lazorse, using it to create a simple API to store and retrieve language specific greetings. The full app can be viewed at https://github.com/BetSmartMedia/Lazorse/blob/master/examples/languages.coffee.
Lazorse uses a “builder” pattern for creating app objects. You supply the function exported by Lazorse with a callback, and it runs that callback in the context of a newly created LazyApp instance. So most apps end up looking something like this:
lazorse = require 'lazorse'
lazorse ->
# Various LazyApp methods here
(This pattern is lifted from Zappa, which lifted it from Sinatra and/or Rack)
The most unusual piece of lazorse is the routing syntax: it’s the same as the draft spec for URI templates, but adds parameter matching semantics on top. (Details and disclaimers).
You declare routes to resources using the LazyApp.resource method:
lazorse.server port: 3001, ->
greetingLookup = english: "Hi", french: "Salut"
# This defines a resource that accepts both GET and POST
@resource "/greeting/{language}":
description: "Per-language greetings"
shortName: 'localGreeting'
GET: -> @ok greeting: greetingLookup[@language], language: @language
POST: ->
if @req.body?.greeting
greetingLookup[@language] = @req.body.greeting
@ok('ok')
else
@error 'InvalidParameter', 'greeting', @req.body?.greeting
All of the keys are optional, but chances are you want to include at least one HTTP method handler, or your resource will be unreachable. The handlers are called with this set to a special context that contains all of the matched URI template parameters and a few other things (see Handler and coercion contexts below for details)
Lazorse also creates a default index (/) resource. This resource responds to GET with an array of all named resources with their URI template and other metadata, such as a description and examples if they are available. So our app will return an array that looks like this:
[
{
"template": "/{language}/greetings",
"description": "Per-language greetings",
"shortName": "localGreeting",
"methods": ["GET", "POST"]
}
]
A resource with no shortName property will not show up in the index.
Coercions are a direct rip-off of the app.param() functionality of Express
Coercion callbacks can be added anywhere in your app using the LazyApp.coerce method. The coercion will be called if a URI template matches req.url and captures a parameter of the same name. This example adds a coercion to our greeting app that will restrict the language parameter to only pre-defined languages:
@coerce "language", """
A language to use for localized greetings.
Valid values: #{Object.keys(greetingLookup).join(', ')}.
""", (language, next) ->
language = language.toLowerCase()
unless greetingLookup[language]
errName = @req.method is 'GET' and 'NotFound' or 'InvalidParameter'
@error errName, 'language', language
else
next null, language
This example also makes use of named errors.
Each handler and coercion is called back in a specially prepared context.
The context is made up of 2 objects in a delegation chain:
An object containing URI Template variables, which delegates to:
A request context object containing:
All helpers defined for the app using LazyApp.helper.
app: The lazorse app
req: The request object from node (via connect)
res: The response object from node (via connect)
error: A callback that will return an error to the client.
- next: A callback that will pass this request to the next middleware
in the chain (via connect).
Because this is a delegation chain, you need to be careful not to mask out helper names with variable names.
Although the example handlers have taken no parameters, lazorse does pass them one parameter: this request context. This is meant to enable fat-arrow handlers in situations where that’s more convenient.
Lazorse includes no templating. Instead, rendering is handled as another middleware in the stack. The default rendering middleware supports JSON and (very ugly) HTML. It inspects the Accept header to see what the client wants, and falls back to JSON when it can’t provide it. You can easily add or override the renderer for a new content type like so:
# Uses https://github.com/visionmedia/consolidate.js
# and a non-standard resource property ``view``
consolidate = require 'consolidate'
@render 'text/html', (req, res, next) ->
res.setHeader 'Content-Type', 'text/html'
engine = req.resource.view?.engine or 'swig'
path = req.resource.view?.path or req.resource.shortName or 'fallback.html'
consolidate[engine] path, res.data, (err, html) ->
if err? then next err else res.end html
With this in place, we could add a view property to any resource and have it rendered to html using a template:
# a room with a view
@resource '/room/{id}':
view: {engine: 'eco', path: 'room.eco'}
GET: -> ...
The default error handler middleware will recognize any error with a code and message property and return an appropriate response.
If an error does not have these properties and the passErrors property of the app is set to false (the default), a generic 500 response will be returned. If passErrors is true, then the error will be passed to the next middleware in the stack.
There is also an @error helper function made available to handlers and coercions to help with returning errors. It can look up errors by name so that you don’t need to manually import your error types if you don’t want to.
To register a new error type with a callback, we use the LazyApp.error method:
# named function.
class TeapotError
constructor: ->
# and register it with the app
@error TeapotError, (err, req, res, next) ->
res.statusCode = 418
res.end """
I'm a little teapot, short and stout.
Here is my handle, here is my spout.
When I get all steamed up, hear me shout!
Tip! me over and pour me out!
"""
@resource '/teapot': GET: -> @error 'TeapotError'
Now our handlers (and coercions) can return 418s easily, no matter what file they are defined in.
You can include an array of example requests for a named route “inline” with the examples property. Each example should be an object with a method, vars, and (optional) body property. In our example app that would look like:
lazorse.server port: 3001, ->
greetingLookup = english: "Hi", french: "Salut"
# This defines a resource that accepts both GET and POST
@resource "/greeting/{language}":
description: "Per-language greetings"
shortName: 'localGreeting'
GET: -> @ok greeting: greetingLookup[@language], language: @language
POST: ->
if @req.body?.greeting
greetingLookup[@language] = @req.body.greeting
@ok('ok')
else
@error 'InvalidParameter', 'greeting', @req.body?.greeting
examples: [
{method: 'GET', vars: {language: 'english'}}
{method: 'POST', vars: {language: 'english'}, body: "howdy"}
]
The route /examples/{shortName} will respond with the example request expanded into a full URI path, so GET /examples/localGreeting would return:
[
{
"method": GET",
"path": "/english/greetings"
},
{
"method": "POST",
"path": "/english/greetings",
"body": "howdy"
}
]