Seneca

A Node.js toolkit for building Minimum Viable Products

This toolkit lets you focus on the real, "business" code of your app. Instead of worrying about which database to use, how to structure your components, or how to manage dependencies, you can just start coding.

You write everything as a command. Your commands get called whenever they match a set of properties. Your calling code doesn't know, or care, which command gets the work done. A JavaScript object goes in, and one comes out, asynchronously.

For practical details, visit the Seneca github project page.

Here's a command that sums two numbers:

var seneca = require('seneca')()

seneca.add( {role:'math', cmd:'sum'}, function(args,callback) {
  var sum = args.left + args.right
  callback(null,{answer:sum})
})

seneca.act( {role:'math', cmd:'sum', left:1, right:2}, function(err,result) {
  if( err ) return console.error( err )
  console.log(result)
})

seneca.add adds a new command. seneca.act acts on the provided pattern, running a command if it matches. Try the code:

$ node example.js
{ answer: 3 }

You registered the pattern {role:'math', cmd:'sum'}. The input matched, and your code ran. The left and right arguments were then used by your plugin to do some real work.

Let's add another command:

seneca.add( {role:'math', cmd:'product'}, function(args,callback) {
  var product = args.left * args.right
  callback(null,{answer:product})
})

seneca.act( {role:'math', cmd:'product', left:3, right:4}, 
            function(err,result) {
              if( err ) return console.error( err )
              console.log(result)
            })
$ node example.js
{ answer: 3 }
{ answer: 12 }

You can add new commands whenever you like, wherever you like.

This named argument style is fun, but if you want a real API, you can have that too:

function print(err,result) {console.log(result)}

var math = seneca.pin({role:'math',cmd:'*'})
math.sum( {left:1,right:2}, print )
math.product( {left:1,right:2}, print )

By pinning the pattern {role:'math',cmd:'*'} you created an object with methods that matched all the cmd properties.

Why use Seneca?

Seneca provides plugins - sets of commands - that look after the foundations of your app for you:

  • Organize your business logic
  • Network API
  • Data layer
  • Caching
  • Logging
  • User management
  • Distributed processing
You write your own plugins and commands using the same system.

Seneca is not a web framework, it's a business logic organizer. You can use it with any web framework.

Here's an example of how this helps you. Let's say the product command needs to live on a separate server. You can move it there with no changes to your calling code. First, set up the server:

seneca.add({role:'math', cmd:'product'}, function(args,callback) {
  var product = args.left * args.right
  callback(null,{answer:product})
})

seneca.use('transport')

var connect = require('connect')
var app = connect()
  .use( connect.json() )
  .use( seneca.service() )
  .listen(10171)

The seneca.use method loads in a plugin. In this case, the transport plugin, which can accept commands over a HTTP interface. Seneca exposes any plugin HTTP interface by returning a middleware function from the seneca.service method. You can use this directly with the connect or express modules.

On the client side, just transport the remote patterns:

seneca.use('transport',{
  pins:[ {role:'math',cmd:'product'} ]
})

seneca.act( {role:'math', cmd:'product', left:3, right:4}, 
            function(err,result) {
              if( err ) return console.error( err )
              console.log(result)
            })

The pins option to the transport plugin accepts a set of patterns that will be sent over to the server (running on localhost by default). The transport plugin just calls seneca.add for each pin, and proxies the input and output to the remote server.

Your calling code, the seneca.act call, does not change. You now have a distributed system with no refactoring. Fire up a few math.product servers, put them behind a load balancer, and now you have a scalable system too.

But what about data storage?

You really need a common database layer so that plugins and commands can all deal with data in the same way. Let's go with an Active Record style - it's nice and easy.

var product = seneca.make('product')
product.name = 'apple'
product.price = 100

product.save$(function( err, product ) {
  if( err ) return console.error( err )
  console.log( 'saved: '+product )

  // product.id was generated for you
  product.load$({id:product.id},function( err, product ) {
    if( err ) return console.error( err )
    console.log( 'loaded: '+product )
  })
})

The make method creates an "Active Record"-ish object for you. The properties of this object store your data. Call the save$ and load$ methods to save and load your data from the database. You also get the list$ method for queries, and remove$ for deleting.

You get an in-memory data store automatically, so the above code works out of the box without any database configuration. Here's how you would use MongoDB:

seneca.use( 'mongo-store', 
            {name:'mydata',host:'127.0.0.1',port:27017} )

seneca.ready( function(err,seneca){
  if( err ) return console.error( 'ERROR:'+err )

  var product = seneca.make('product')
  product.name = 'apple'
  product.price = 100

  product.save$(function( err, product ) {
    if( err ) return console.error( err )

    // product.id is a MongoDB id this time
    product.load$({id:product.id},function( err, product ) {
      if( err ) return console.error( err )
      console.log( 'loaded: '+product )
    })
  })
})

The seneca.use method tells Seneca to load a plugin, including any commands the plugin exposes. In this case, you're loading the mongo-store plugin, which lets you talk to the MongoDB database.

To use the mongo-store plugin, you'll need to install it:

npm install seneca-mongo-store

The seneca.ready method waits for the database connection to be established. Apart from plugin configuration, your data logic code is exactly the same. Swapping databases at any stage of your project is trivial.

How is the database layer implemented? It's just another set of commands. Here's the same code again:

var product = seneca.make('product')
product.name = 'apple'
product.price = 100

seneca.act( 
  { role:'entity', cmd:'save', ent:product},
  function( err, product ) {
    if( err ) return console.error( err )
    console.log( 'saved: '+product )

    seneca.act( 
      // q is: the query - find matching property values
      // qent is: entity type we want back
      { role:'entity', cmd:'load', q:{id:product.id}, qent:product},
      function( err, product ) {
        if( err ) return console.error( err )
        console.log( 'loaded: '+product )
      })
  })

You can add support for your favorite database by writing a plugin with commands for each of the entity operations: save, load, etc. Here are some of the data stores Seneca supports:

You can even decide to save different entities into different databases - for example, put user profiles into MongoDB, purchase transactions into MySQL, and sessions into Redis. You do need to stay database independent by avoiding table joins, but you don't need them anyway!