A Seneca.js Web plugin
This plugin provides a web service API routing layer for Seneca action patterns. It translates HTTP requests with specific URL routes into action pattern calls. It's a built-in dependency of the Seneca module, so you don't need to include it manually. Use this plugin to define your web service API.
This plugin supports Express-style middleware. This plugin supports (starting from version 0.5.0) also hapi, see bellow usage example.
- Node: 0.10, 0.12, 4, 5
- License: MIT
seneca-web's source can be read in an annotated fashion by,
- running
npm run annotate
- viewing online.
If you're using this module, and need help, you can:
- Post a github issue,
- Tweet to @senecajs,
- Ask on the Gitter.
If you are new to Seneca in general, please take a look at senecajs.org. We have everything from tutorials to sample apps to help get you up and running quickly.
Supports Seneca versions 1.x and 2.x
This plugin module is included in the main Seneca module:
npm install seneca
The primary purpose of this plugin is to define URL routes that map to action patterns. This lets you turn your action patterns into a well-defined web service API.
- Use a separate set of action patterns for your web service API. Don't expose your internal patterns! *
The role:web
action accepts a use
parameter that is a declarative
definition of a set of routes. You specify a set of action patterns
that will be exposed, and the URL routes that map to each
pattern. Each call to the role:web
action defines a middleware
service with a set of routes. Incoming requests are passed to each
service in turn until one matches. If none match, the request is
passed onwards down the middleware chain.
The use
parameter can also be an Express-style middleware function
of the form function( req, res, next )
. You are then free to handle
the action mapping yourself.
To use the services in your app, call seneca.export('web')
to obtain
a wrapper middleware function that performs the route matching.
Note: Hapi implementation is supported only for node v4 or later.
This example defines some API end point URLs that correspond to Seneca actions:
GET /my-api/zig
:role:api,cmd:zig
GET /my-api/bar
:role:api,cmd:bar
GET /my-api/qaz
:role:api,cmd:qaz
POST /my-api/qaz
:role:api,cmd:qaz
var Chairo = require ( 'chairo' )
var Hapi = require ( 'hapi' )
var server = new Hapi.Server ()
server.connection()
server.register(
{
register: Chairo,
options: {
// options for Seneca
}
}, function ( err ) {
var seneca = server.seneca
seneca.add('role:api,cmd:zig',function(args,done){
done(null,{bar:'g'})
})
seneca.add('role:api,cmd:bar',function(args,done){
done(null,{bar:'b'})
})
seneca.add('role:api,cmd:qaz',function(args,done){
done(null,{qaz:'z'})
})
seneca.act('role:web',{use:{
// define some routes that start with /my-api
prefix: '/api',
// use action patterns where role has the value 'api' and cmd has some defined value
pin: {role:'api',cmd:'*'},
// for each value of cmd, match some HTTP method, and use the
// query parameters as values for the action
map:{
zig: true, // GET is the default
bar: {GET:true}, // explicitly accepting GETs
qaz: {GET:true,POST:true} // accepting both GETs and POSTs
}
}})
}
} )
This example defines some API end point URLs that correspond to Seneca actions:
GET /my-api/zig
:role:api,cmd:zig
GET /my-api/bar
:role:api,cmd:bar
GET /my-api/qaz
:role:api,cmd:qaz
POST /my-api/qaz
:role:api,cmd:qaz
var seneca = require('seneca')()
seneca.add('role:api,cmd:zig',function(args,done){
done(null,{bar:args.zoo+'g'})
})
seneca.add('role:api,cmd:bar',function(args,done){
done(null,{bar:args.zoo+'b'})
})
seneca.add('role:api,cmd:qaz',function(args,done){
done(null,{qaz:args.zoo+'z'})
})
seneca.act('role:web',{use:{
// define some routes that start with /my-api
prefix: '/my-api',
// use action patterns where role has the value 'api' and cmd has some defined value
pin: {role:'api',cmd:'*'},
// for each value of cmd, match some HTTP method, and use the
// query parameters as values for the action
map:{
zig: true, // GET is the default
bar: {GET:true}, // explicitly accepting GETs
qaz: {GET:true,POST:true} // accepting both GETs and POSTs
}
}})
var express = require('express')
var app = express()
// This is how you integrate Seneca with Express
app.use( seneca.export('web') )
app.listen(3000)
// run: node test/example.js --seneca.log=type:act
// try: curl -m 1 -s http://localhost:3000/my-api/bar?zoo=a
// returns {"bar":"ab"}
// try curl -m 1 -s -H 'Content-Type: application/json' -d '{"zoo":"b"}' http://localhost:3000/my-api/qaz
// returns {"qaz":"bz"}
You can use Seneca directly from with your route handlers by just
calling seneca.act
directly. You don't need to use seneca-web
at
all. This approach may be more convenient if you already have a larger
scale application architecture, or are integrating Seneca into an
existing system.
This plugin does not provide an access control feature, or protect you from attacks such as request forgery. However, since it does support the middleware pattern, you can use other middleware modules to provide these features.
Define a web service as a mapping from URL routes to action patterns.
Parameters
use
: mapping object, or middleware function
When use
is a function of the form function(req,res,next) { ... }
it is considered to be a middleware function, and placed into the list
of middleware provided by the seneca-web plugin.
Use this approach when you need to write special case custom code. The
req
parameter will contain a seneca
property that gives you access
to the Seneca instance bound to the current HTTP request. In a HTTP
middleware context it's important to use the request specific Seneca
instance, as other plugins may have added context to that
instance. See, for example: seneca-user.
seneca.add('zed:1',function(args,done){
done(null,{dez:2})
})
seneca.act('role:web', {use: function( req, res, next ){
if( '/zed' == req.url ) {
// NOTE: req.seneca reference
req.seneca.act('zed:1',function(err,out){
if(err) return next(err);
// assumes an express app
res.send(out)
})
}
else return next();
}})
The action mapping object is a convenience format for declarative definition of a HTTP API based on Seneca actions. You can see examples of this use-case in these projects:
You specify a set of action patterns, and the URL routes that map to these patterns. The set of patterns is specified using a pin, an example pattern that includes wildcards for some properties.
For example, if you have defined the patterns:
seneca.add( 'role:color,cmd:red', ... )
seneca.add( 'role:color,cmd:green', ... )
seneca.add( 'role:color,cmd:blue', ... )
seneca.add( 'role:sound,cmd:piano', ... )
Then the pin role:color,cmd:*
will pick out the first three patterns:
role:color,cmd:red
role:color,cmd:green
role:color,cmd:blue
but not role:sound,cmd:piano
as that does not match the pin pattern.
A simple mapping can then be defined as follows:
seneca.add('role:color,cmd:red', function( args, done ){
done( null, {color:'#F00'} )
})
seneca.add('role:color,cmd:green', function( args, done ){
done( null, {color:'#0F0'} )
})
seneca.add('role:color,cmd:blue', function( args, done ){
done( null, {color:'#00F'} )
})
seneca.act('role:web', {use:{
prefix: '/color', // the URL prefix for the web service API
pin: 'role:color,cmd:*', // the set of patterns to map
map: {
red: true, // GET /color/red triggers role:color,cmd:red
green: true,
blue: true,
}
}})
Which creates an HTTP API that responds like so:
$ curl -m 1 -s http://localhost:3000/color/red
{"color":"#F00"}
$ curl -m 1 -s http://localhost:3000/color/green
{"color":"#0F0"}
$ curl -m 1 -s http://localhost:3000/color/blue
{"color":"#00F"}
The properties of the mapping define the routes and the action patterns to call:
prefix
: prefix string for the URL, in this case /colorpin
: the pin that selects the actionsmap
: each property of this sub-object should correspond to a matched wildcard value, in this case: red, green, and blue
The map entries define the nature of the route. In the above example, the default case is to respond to HTTP GET requests. The mapping forms URLs by appending the name of the wildcard value to the prefix to form the full URL. So you end up with these endpoints:
GET /color/red
GET /color/green
GET /color/blue
To respond to POST requests, do this:
seneca.act('role:web', {use:{
prefix: '/color',
pin: 'role:color,cmd:*',
map: {
red: { POST:true }
}
}})
The argument properties for your action are built from the URL parameters, query string, and body data, merged in that order of precedence. You can modify this behaviour with the useparams and usequery settings, as described below.
Example:
// just echo the args back out again!
seneca.add('role:api,cmd:echo', function( args, done ){
done( null, args )
})
seneca.act('role:web', {use:{
prefix: '/api',
pin: 'role:api,cmd:*',
map: {
echo: {POST:true, suffix:'/:foo'}
}
}})
Which behaves as follows:
$ curl -m 1 -s -H 'Content-Type: application/json' -d '{"zed":"c"}' \
http://localhost:3000/api/echo/a?bar=b
{"cmd":"echo","role":"api","zed":"c","foo":"a","bar":"b"}
Note: you do not have to list all the matching wildcards in a map. Only those you list explicitly will be supported.
The wildcard mapping object accepts the following optional properties that let you refine the route specification:
- METHOD: any HTTP method (GET, POST, PUT, DELETE, etc); the value can be:
- true: accept requests with this method.
- a middleware function: this allows you to completely customize the route.
- a method specification, see below.
- alias: custom URL path, concatenated to top level prefix; can contain Express-style route parameters: /api/:bar sets req.params.bar.
- suffix: appended to route URL.
- useparams: merge any URL parameter values into the arguments for the Seneca action; default:
true
- usequery: merge any URL query values into the arguments for the Seneca action; default:
true
- dataprop: provide all request parameters inside a
data
property on the Seneca action, default:false
- redirect: perform a 302 redirection with the value as the new location URL.
- handler: function that translates inbound requests to Seneca actions, see below.
- responder: function that translates outbound Seneca results into HTTP response data, see below.
- modify: function that modifies the output object in some way (usually to delete sensitive fields), see below.
The response object that you provide to the seneca-web plugin via a
Seneca action response, can contain a http$
object to control the
HTTP response. This object can have the optional properties:
- status: set the HTTP status code
- headers: sub-object defining custom header values
- redirect: redirect URL
For each HTTP method, you can provide a method specification (as a sub-object) that overrides some of the route specification. Note that URL cannot be modified at the method level - that would be a different route! In particular this means that the alias can only be set at the route specification level. If you need other behaviour, your best option is to write a custom middleware function, as noted above.
The method specification can contain the following properties, which override the route specification:
* _useparams_
* _usequery_
* _dataprop_
* _handler_
* _responder_
* _modify_
Example:
seneca.act('role:web', {use:{
prefix: '/color',
pin: 'role:color,cmd:*',
map: {
red: { POST:{dataprop:true} }
}
}})
This function has the form: function( req, res, args, act, respond )
, where:
- req: Express Request object
- res: Express Response object
- args: Seneca action arguments derived from the HTTP request parameters
- act: the Seneca action function, call with
act(args,respond)
- respond: call to return a result:
respond(err,out)
Provide your own handler function when you need to customize the behaviour of a route.
This function has the form: function( req, res, err, out )
, where:
- req: express Request object
- res: express Response object
- err: Seneca action error, if any
- out: Seneca action result, if any
Provide your own responder function when you need to customize the exact data of the HTTP response.
The output of a Seneca action is an object that is serialized to JSON
and returned over HTTP. It is normally necessary to remove unwanted
properties that should not appear to network clients. The default
modifier removes all properties that contain a $
, as these are used
by Seneca for internal meta control. The http$
is preserved, as this
is used (and removed) by the default responder. Provide your own
modifer function to customize this behavior.
At the top level, you can also provide general middleware functions that get called before the mapping handlers are executed. These functions allow you to perform shared operations, such as extracting a cookie token, or attaching meta data to Request objects.
seneca.act('role:web', {use:{
prefix: '/color',
pin: 'role:color,cmd:*',
startware: function(req,res,next){
// attach something to every request
req.foo = "bar"
next()
}
premap: function(req,res,next){
// attach something only to mapped requests
req.zed = "qux"
next()
}
map: {
red: { POST:true }
}
}})
These general middleware functions are:
- startware: always executed, before any mappings, even when there is no route match
- premap: only executed when a mapping matches, and before the mapping; can also be used at the mapping level for route-specific behaviour
- postmap: executed only when a custom mapping middleware calls
next
.
The primary advantage of using the mapping specification over a custom
middleware function is that seneca-web maintains a list of mapped
routes, and also performance statistics for those routes. Each time
you call role:web
you define a service, and the service defines a
number of routes.
You can see some (admittedly terse) examples of mapping specifications in the test/test-server.js and test/test-client.js testing code, or in the example applications noted above (nodezoo.com etc).
This command returns an array of all of the routes that have been defined.
seneca.act('role:web, cmd:routes', function(err, routes) {
console.log(routes);
});
Each route is described as an object with properties:
- url: the route URL
- method: the HTTP method
- srv: the service that defined the route
This command returns an array of all of the service functions that have been defined.
seneca.act('role:web, cmd:list', function(err, services) {
console.log(service);
});
This command is used to send processing response to Express. A default implementation is provided. This implementation will take into account that if error is reported then the next middleware implementations will not be called and also http$.redirect and http$.status are used. This action can be used to implement another desired behavior.
Parameters:
- req - web request
- res - web response
- err - processing error
- response - processing response
- next - next function to be used to call the next middleware registered in Express.
The Senecajs org encourage open participation. If you feel you can help in any way, be it with documentation, examples, extra testing, or new features please get in touch.
To test, use:
npm test
To install separately (if you're using a fork or branch, say), use:
npm install seneca-web
And in your code:
var seneca = require('seneca')({
default_plugins:{
web:false
}
})
seneca.use( require('seneca-web') )
Copyright (c) 2013 - 2016, Richard Rodger and other contributors. Licensed under MIT.