REST APIs with Node Tastypie - Part 3: Custom Routes

W

e have been taking a look at how to use Tastypie to easily create robust, feature rich REST APIs. Our focus has been on CRUD operations, mainly because they are the ones that ship with tastypie. However, we are not restricted to CRUD with tastypie. You can define any number of endpoints to do whatever you want. To create a custom set of endpoints there are three basic things you need to do:

  • Define a route
  • Define a handler
  • Define Method Access

Last time we wrote a very simple resource that allowed use to just return simple objects. We are going to continue with that example by adding some custom routes to it. If you are joining in late, here is the example as we left it last time.

var Resource = require('tastypie').Resource; 

var Simple = Resource.extend({  
    get_list: function( bundle ){ 
        // use the respond method if you 
        // want serialization, status code, etc... 
        bundle.data = { key:'value' }; 
        return this.respond( bundle ) 
    } 
    , get_detail: function( bundle ){ 
        // or just send a straight response. 
        return bundle.res({succes:1}).code( 200 ) 
    } 
    , put_detail: function( bundle ){ 
        return bundle.res({any:'data you want'}).code( 202 ) 
    } 
    , post_list: function( bundle ){ 
        var data = bundle.req.payload; 
        // do something with the data. 
        return bundle.res({any:'data you want'}).code( 201 ) 
    } 
    , delete_detail: function( bundle ){ 
        return bundle.res().code( 204 ) 
    } 
});

Define A Route

For this example we are going to add a sub resource onto the detail endpoint ( /api/v1/simple/{id}/foobar ). Resources have two instance methods that are used to create route definitions, base_urls and prepend_urls. base_urls is the method that defines the list, detail and schema routes. You don't really need to worry about this one unless you want to remove something. prepend_url returns an array of objects that gets added on to the base urls. By default it returns an empty array, so we just need to fill it in. Each definition in the array needs to have a name, route and handler property

propertyusage
path a full relative url
name A unique name of the endpoint
handler A route handler for hapi.js

So we just define the prepend_urls method here:

var Resource = require('tastypie').Resource; 

var Simple = Resource.extend({  
    prepend_urls: function( ){
        return[{
           path: '/api/v1/simple/{pk}/foobar',
           name:'foobar',
           handler: this.dispatch_foobar.bind( this )
        }]
    }
});

Define A Handler

What the handler function does is really up to you. This method is used as the Hapi route handler, and will be passed the request and reply object directly from hapi. However, to take the advantage of all of the tastypie infrastructure, you will want to funnel everything through the dispatch function on the resource. In the example above, we have told the resource to call dispatch_foobar, which would look something like this

, dispatch_foobar: function( req, reply ){
   // Do some custom logic & fancy magic

   return this.dispatch( 'foobar', this.bundle( req, reply ) );
}

That is it - dispatch kicks off the rest; throttling, method checks, caching, and determines the right instance method to call, <HTTPVERB>_<ACTION>. For example, to handle GET requests for particular example, we need to define a get_foobar method. You can do what ever you really want, but the general pattern is to give the request bundle a data property, and pass it to the respond instance method. The respond method handles content negotiation, serialization and status codes.

, get_foobar: function( bundle ){
   // all the magic you want
   bundle.data = { foo: 'bar' };
   return this.respond( bundle, tastypie.http.ok );
}

If you want to make use of defined fields for advanced data preperation, you can use the hydrate ( incoming ) / dehydrate ( outgoing ) methods on the instance.

, get_foobar: function( bundle ){
   // all the magic you want
   getData( function( err, data ){
       this.full_dehydtate( data, bundle, function(e, dhyd ){
             bundle.data = dhyd;
             return this.respond( bundle );
       }.bind( this ) );
   }.bind( this ) );
}

Define Method Access

A resource allows you to specifiy which HTTP Verbs are allowed for each action that is defined, foobar in our case. To do that you can define an object in options, <ACTION>MethodsAllowed where the keys are the verb, and the values is a Boolean. So too allow get requests, we set the get property to true.

If the property is set to false, tastypie will automatically respond with a 405 Method Not Allowed. If the property is true, but you have not defined the appropriate instance methods, tastypie will respond with a 501 Not Implemented.

NOTE: things in options are configurable when an instance of your resource is created ( new Simple( { options } ) ). they are also inherited by sub classes,

var Simple = Resource.extend({  
    options:{
       foobarMethodsAllowed:{ get: true }
    }
});

That is all there is to it. You can really be as simple or as complex as you need and/or want to be, and make use of as much tastypie as you want. Our final resource with a custom foobar action might look like this:

var Resource = require('tastypie').Resource; 

var Simple = Resource.extend({  
    options:{
       foobarMethodsAllowed: { get: true }
    }

   , fields: {
      // default field definitions
   }

    , get_list: function( bundle ){ 
        // use the respond method if you 
        // want serialization, status code, etc... 
        bundle.data = { key:'value' }; 
        return this.respond( bundle ) 
    } 

    , get_detail: function( bundle ){ 
        // or just send a straight response. 
        return bundle.res({succes:1}).code( 200 ) 
    } 

    , put_detail: function( bundle ){ 
        return bundle.res({any:'data you want'}).code( 202 ) 
    } 

    , post_list: function( bundle ){ 
        var data = bundle.req.payload; 
        // do something with the data. 
        return bundle.res({any:'data you want'}).code( 201 ) 
    } 

    , delete_detail: function( bundle ){ 
        return bundle.res().code( 204 ) 
    }

   // CUSTOM Resource Action setup
   , prepend_urls: function( ){
        return[{
           path: '/api/v1/simple/{pk}/foobar',
           name:'foobar',
           handler: this.dispatch_foobar.bind( this )
        }]
    }

   , dispatch_foobar: function( req, reply ){
      // Do some custom logic & fancy magic

      return this.dispatch( 'foobar', this.bundle( req, reply ) );
   }

   , get_foobar: function( bundle ){
      // all the magic you want
      getData( function( err, data ){
         this.full_dehydtate( data, bundle, function(e, dhyd ){
            // set data & respond
            bundle.data = dhyd;
            return this.respond( bundle );
         }.bind( this ) );
      }.bind( this ) );
   }
});
tastypie REST hapi node