/tastypie

A Robust REST Framework for Node.js & Hapi - Inspired by Django - Tastypie

Primary LanguageJavaScriptMIT LicenseMIT

node tastypie

NPM

license npm node David Travis Codacy grade

A re-realization of the popular Django REST framework - Tasypie for Node.js and Hapi.js. Looking for active contributors / collaborators to help shape the way people build APIs!

API Documentation Working Example

Officially Supported Resources Types

Experimental Resource Types

Create a simple Api

const {Resource, Api} = require('tastypie')
    , Api      = tastypie.Api
    , hapi     = require('hapi')
    , server   = new hapi.Server()
    , v1       = new Api('api/v1' )
    ;

Resource = tastypie.Resource.extend({
    fields:{
        lastName:{ type:'char', attribute:'name.first', help:'last name of the user'},
        firstName:{type:'char', attribute: 'name.last', help:'first name of the user'}
    }
})

v1.use('test', new Resource() );

server.connection({port:2000, host:'localhost'});

server.register( v1, function( ){
    server.start(function(){
        console.log('server listening localhost:2000');
    });
});

Self Describing

Tastypie exposes endpoint to descibe available resources and the contracts they expose

Resource listing

// GET /api/v1
{
    "test":{
        "schema": "/api/v1/test/schema",
        "detail": "/api/v1/test/{pk}",
        "list": "/api/v1/test"
    }
}
Auto Schema
// GET /api/v1/test/schema

{
	"filtering": {},
	"ordering": [],
	"formats": ["application/json", "text/javascript", "text/xml"],
	"limit": 25,
	"fields": {
		"lastName": {
			"default": null,
			"type": "string",
			"nullable": false,
			"blank": false,
			"readonly": false,
			"help": "last name of the user",
			"unique": false,
			"enum": []
		},
		"firstName": {
			"default": null,
			"type": "string",
			"nullable": false,
			"blank": false,
			"readonly": false,
			"help": "first name of the user",
			"unique": false,
			"enum": []
		},
		"id": {
			"default": null,
			"type": "string",
			"nullable": false,
			"blank": false,
			"readonly": true,
			"help": "Unique identifier of this resource",
			"unique": false,
			"enum": []
		},
		"uri": {
			"default": null,
			"type": "string",
			"nullable": false,
			"blank": false,
			"readonly": true,
			"help": "The URI pointing back the this resource instance",
			"unique": false,
			"enum": []
		}
	},
	"allowed": {
		"schema": ["get"],
		"detail": ["get", "put", "post", "delete", "patch", "head", "options"],
		"list": ["get", "put", "post", "delete", "patch", "head", "options"]
	}
}
Get Data
// GET /api/v1/test

{

    "meta":{
        "count":1,
        "limit":25,
        "next":null,
        "previous":null 
    },
    "data":[{
        firstName:"Bill",
        lastName:"Bucks",
        uri:"/api/v1/test/1"    
    }]
} 


// GET /api/v1/test/1

{
    firstName:"Bill",
    lastName:"Bucks",
    uri:"/api/v1/test/1"    
}

Built-in Fields

  • field ( ApiField ) - Generic noop field - returns the data as it is given
  • object ( ObjectField ) - Generic no-op field - returns the data as it is given
  • char ( character / CharField ) - Converts values to strings
  • array ( ArrayField ) Converts comma sparated strings into arrays
  • int ( int / IntegerField ) converts numeric values into integers using parseInt
  • float ( FloatField ) Converts values to floating point number using parseFloat
  • bool ( BooleanField ) Forces values to booleans
  • date ( DateField ) Converts date strings to Date objects with out any time data
  • datetime ( DateTimeField ) Attempts to convert date time strings into date objects
  • file ( FileField ) A field that pipes a stream to a configured location, and store a path
  • filepath ( FilePathField ) A field that handles file locations rather than dealing with streams or binary data

This allows for full HTTP support and basic CRUD operations on a single enpoint - api/v1/test

curl -XPOST -H "Content-Type: applciation/json" -d '{"test":"fake"}' http://localhost:3000/api/v1/test
curl -XPUT  -H "Content-Type: applciation/json" -d '{"test":"real"}' http://localhost:3000/api/v1/test
curl -XDELETE http://localhost:3000/api/v1/test/fake

HTTP Verbs

This is how tastypie handles the base HTTP Methods

  • GET returns a list of resource instance or a specific resource instance
  • DELETE removes a specific resource instance
  • PUT REPLACES a resource instance. This is not a partial update. Any optional fields not defined will be set to undefined
  • PATCH a PARTIAL update to a specific resource instance. Only the fields sent in the payload are dealt with.
  • OPTIONS returns an empty response with the Allow header set
  • HEAD, TRACE, and CONNECT are left to implementation on a per resource bases

Serialization

The base serializer can deal with xml, json and jsonp out of the box. Serialization method is determined by the Accept header or a format query string param

curl -H "Accept: text/xml" http://localhost:3000/api/v1/test
curl http://localhost:3000/api/v1/test?format=xml
<?xml version="1.0" encoding="UTF-8"?>
<response>
 <firstName type="string">bill</firstName>
 <lastName type="string">Schaefer</lastName>
</response>

NOTE: hapi captures application/foo so for custom serialization, we must use text/foo

Quick & Dirty Resource

A functional resource, by convention, should define method handlers for each of the actions ( list, detail, schema, etc ) & HTTP verbs where it makes sense - where the resource method name is <VERB>_<ACTION>.

const tastypie = require('tastypie')
const Resource = tastypie.Resource;
const http     = tasypie.http
const Simple;

const Template = function(){
    this.key = null
    this._id = null
}

Template.prototype.save = function( cb ){
    setImmediate(cb, this)
}

Template.prototype.toJSON = function(){
    return {key, _id}
}

Simple = Resource.extend({
    options:{
    	name:'simple'
    	,pk:'_id'
        ,template: tempalte
    }
    ,fields:{
        key:{type:'char'}
    }
    
    ,constructor:function( options ){
        this.parent('constructor', options)
    }
    
    /**
     * handles DELETE /{pk}
     **/
    , delete_detail: function( bundle ){
        bundle.data = null;
        return this.respond( bundle, http.noContent ) // Send a custom response code
    }

    /**
     * handles GET /{pk}
     **/    
    , get_detail: function( bundle ){
        bundle.data = {key:'foo', _id:1};
        return this.respond( bundle ) // defaults to 200 OK respose code
    }
    
    /**
     * handles GET /
     **/
    ,get_list: function( bundle ){
        // the data property is what gets returned
        bundle.data = [{ key:'foo', _id:1},{key:'bar', _id:2}]; 

        // use the respond method if you
        // want serialization, status code, etc...
        return this.respond( bundle )
    }
    
    /**
     * handles PATCH /{pk}
     **/
    , patch_detail: function( bundle ){
        // or just send a straight response.
        // res is the hapi reply object
        return bundle.res({any:'data you want'}).code( 201 );
    }    

    /**
     * handles POST /
     **/
    , post_list: function( bundle ){
        var data = bundle.req.payload;
        // do something with the data.
        let format = this.format( bundle );
        this.deserialize(bundle.data, format, ( err, data ) => {
            bundle.data = data;
            this.full_hydrate( bundle, ( err, bundle ) => {
                bundle.object.save();
                this.full_dehydrate( bundle.object, ( err, dehydrated ) => {
                    bundle.data = dehydrated;
                    this.respond( bundle, http.created );
                }) 
            })
        })
        return this.respond( bundle, http.created)
    }
    
    /**
     * handles PUT /{pk}
     **/
    , put_detail: function( bundle ){
    	// Manually set the Bundle's data property to send back to the client
        bundle.data = {key:'updated'}
        return this.respond( bundle, http.accepted )
    }
});

Example FS resourse

The base resource defines many of the required <VERB>_<ACTION> methods for you and delegates to smaller internal methods which you can over-ride to customize behaviors. Here is a resource that will asyncronously read a JSON file from disk are respond to GET requests. Supports XML, JSON, paging and dummy cache out of the box.

const hapi                                       = require('hapi');
const fs                                         = require('fs')
const path                                       = require('path')
const {Resource, Api, Fields, Class, Serializer} = require('tastypie')
const Options                                    = Class.Options 
const debug                                      = require('debug')('tastypie:example')

const server = new hapi.Server()


// make a simple object template to be populated during the hydration process
// This could be an ORM Model class just as easily
function Schema(){
    this.name = {
        first: undefined, last: undefined
    }
    this.age = undefined;
    this.guid = undefined;
    this.range = []
    this.eyeColor = undefined;
};


let Base = Resource.extend({
    options:{
        template: Schema // Set the schema as the Object template
    }
    ,fields:{
        // remap _id to id via attribute
          id       : { type:'field', attribute:'_id' }
        , age      : { type:'int' } 

        // can also be a field instance
        , eyeColor : new fields.CharField({'null':true})
        , range    : { type:'array', 'null': true }
        , fullname : { type:'char', 'null':true }

        // remap the uid property to uuid. 
        , uuid     : { type:'char', attribute:'guid'}
        , name     : { type:'field'}
    }
   , constructor: function( meta ){
        this.parent('constructor', meta )
   }

    // internal lower level method called by get_list responsible for getting the raw data
    , get_objects: function(bundle, callback){
        fs.readFile( path.join(__dirname, 'example','data.json') , callback)
    }


    // internal low level method called by post_detail reponsible for dealing with a POST request
    , create_object: function create_object( bundle, opt, callback ){
        bundle = this.full_hydrate( bundle )
        callback && callback(null, bundle )
    }

    // per field dehydration method - generates a full name field from name.first & name.last
    , dehydrate_fullname:function( obj, bundle ){
        return obj.name.first + " " + obj.name.last
    }

    // top level method for custom GET /upload 
    , get_upload: function( bundle ){
        this.respond({data:{key:'value'}})
    }

    // method called by get_detail that retreives an individual object by id.
    // becuase it's in a flat file, read it, filter and return first object
    ,get_object: function(bundle, callback){
        this.get_objects(bundle,function(e, objects){
            var obj = JSON.parse( objects ).filter(function( obj ){
                return obj._id = bundle.req.params.id
            })[0]
            callback( null, obj )
        })
    }

    // Proxy method for delegeting HTTP methods to approriate resource method
    // as defined below
    , dispatch_upload: function(req, reply ){
        // Do additional magic here.

        // dispatch will call <HTTP_METHOD>_upload
        return this.dispatch('upload', this.bundle( req, reply ) )
    }
    
    // adds a custom route for upload in addition to standard crud methods
    , prepend_urls:function(){
        return [{
          route: '/api/v1/data/upload'
          , handler: this.dispatch_upload.bind( this )
          , name:'upload'
        }]
    }
});
var api = new Api('api/v1', {
    serializer:new Serializer()
})

app.connection({port:process.env.PORT || 2000 });

api.user('data', new Base() );

app.register( api, function(e){
    app.start(function(){
        console.log('server is ready')
    });
});

Now you can read data from a file with your rest API

curl http://localhost:2000/api/v1/test
curl http://localhost:2000/api/v1/test?format=xml
curl http://localhost:2000/api/v1/test/1
curl http://localhost:2000/api/v1/test/2
curl http://localhost:2000/api/v1/test/2?format=xml

Contributing

Contributions & patches welcome. If you have experience working with the original python / django-tastypie, input would be greatly appreciated. Anything from docs, test and feature code is fair game.

  1. Fork
  2. Write Code
  3. Write tests
  4. Document your code
  5. Push
  6. Open Pull Request