From 0c9110ab86a77ddb39066d89eb3a38ef8b75e99c Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 21 Jun 2015 21:48:28 -0600 Subject: [PATCH 1/5] Adding ability to register services that already are event emitters (#118) --- lib/mixins/event.js | 13 ++++++++++--- package.json | 1 + test/distributed.test.js | 41 +++++++++++++++++++++++++++++++++++++++ test/mixins/event.test.js | 37 ++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 test/distributed.test.js diff --git a/lib/mixins/event.js b/lib/mixins/event.js index c8febff67d..eb25c391ee 100644 --- a/lib/mixins/event.js +++ b/lib/mixins/event.js @@ -29,7 +29,9 @@ var EventMixin = { }); _.each(eventMappings, function (event, method) { - if (typeof self[method] === 'function') { + var alreadyEmits = self._serviceEvents.indexOf(event) !== -1; + + if (typeof self[method] === 'function' && !alreadyEmits) { // The Rubberduck event name (e.g. afterCreate, afterUpdate or afterDestroy) var eventName = 'after' + method.charAt(0).toUpperCase() + method.substring(1); self._serviceEvents.push(event); @@ -50,10 +52,15 @@ var EventMixin = { } }; -_.extend(EventMixin, EventEmitter.prototype); - module.exports = function (service) { + var isEmitter = typeof service.on === 'function' && + typeof service.emit === 'function'; + if (typeof service.mixin === 'function') { + if(!isEmitter) { + service.mixin(EventEmitter.prototype); + } + service.mixin(EventMixin); } }; diff --git a/package.json b/package.json index d4906652bc..b485097740 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "uberproto": "^1.1.0" }, "devDependencies": { + "feathers-client": "^0.1.1", "jshint": "^2.6.3", "mocha": "^2.2.0", "q": "^1.0.1", diff --git a/test/distributed.test.js b/test/distributed.test.js new file mode 100644 index 0000000000..e9e0bce8fd --- /dev/null +++ b/test/distributed.test.js @@ -0,0 +1,41 @@ +var assert = require('assert'); +var client = require('feathers-client'); +var io = require('socket.io-client'); +var feathers = require('../lib/feathers'); + +describe('Distributed Feathers applications test', function () { + before(function(done) { + var app = feathers() + .configure(feathers.socketio()) + .use('todos', { + create: function(data, params, callback) { + data.id = 42; + callback(null, data); + } + }); + + app.listen(8888, done); + }); + + it('passes created event between servers', function (done) { + var socket = io('http://localhost:8888'); + var remoteApp = client().configure(client.socketio(socket)); + var todo = { text: 'Created on alpha server', complete: false }; + var beta = feathers() + .configure(feathers.rest()) + .use('todos', remoteApp.service('todos')); + + beta.listen(9999, function() { + beta.service('todos').on('created', function(newTodo) { + assert.deepEqual(newTodo, { + id: 42, + text: 'Created on alpha server', + complete: false + }); + done(); + }); + + socket.emit('todos::create', todo); + }); + }); +}); diff --git a/test/mixins/event.test.js b/test/mixins/event.test.js index dbf20dc7a0..bb64a9ba17 100644 --- a/test/mixins/event.test.js +++ b/test/mixins/event.test.js @@ -3,6 +3,8 @@ var assert = require('assert'); var _ = require('lodash'); var Proto = require('uberproto'); +var EventEmitter = require('events').EventEmitter; + var mixinEvent = require('../../lib/mixins/event'); var EventMixin = mixinEvent.Mixin; var create = Proto.create; @@ -23,7 +25,7 @@ describe('Event mixin', function () { var instance = create.call(FixtureService); assert.equal('Original setup: Test', instance.setup('Test')); - assert.ok(instance._rubberDuck instanceof require('events').EventEmitter); + assert.ok(instance._rubberDuck instanceof EventEmitter); var existingMethodsService = { setup: function (arg) { @@ -32,6 +34,7 @@ describe('Event mixin', function () { }; Proto.mixin(EventMixin, existingMethodsService); + Proto.mixin(EventEmitter.prototype, existingMethodsService); assert.equal('Original setup from object: Test', existingMethodsService.setup('Test')); assert.equal(typeof existingMethodsService.on, 'function'); @@ -149,4 +152,36 @@ describe('Event mixin', function () { assert.equal(data.id, 27); }); }); + + it('does not punch when service has an events list (#118)', function(done) { + var FixtureService = Proto.extend({ + events: [ 'created' ], + create: function (data, params, cb) { + _.defer(function () { + cb(null, { + id: 10, + name: data.name + }); + }); + } + }); + + FixtureService.mixin(EventEmitter.prototype); + mixinEvent(FixtureService); + + var instance = create.call(FixtureService); + instance.setup(); + + instance.on('created', function (data) { + assert.deepEqual(data, { custom: 'event' }); + done(); + }); + + instance.create({ + name: 'Tester' + }, {}, function (error, data) { + assert.equal(data.id, 10); + instance.emit('created', { custom: 'event' }); + }); + }); }); From a4b30be0fbaa60aeb8a1ec00517a0ab874a6bcf2 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 21 Jul 2015 19:26:18 -0600 Subject: [PATCH 2/5] The docs are fine in gh-pages. Updating changelog for 1.1 and readme for new website. --- changelog.md | 4 +- docs/api.md | 664 -------------------------------------------------- docs/faq.md | 177 -------------- docs/guide.md | 630 ----------------------------------------------- package.json | 3 +- readme.md | 274 +-------------------- 6 files changed, 17 insertions(+), 1735 deletions(-) delete mode 100644 docs/api.md delete mode 100644 docs/faq.md delete mode 100644 docs/guide.md diff --git a/changelog.md b/changelog.md index fae0a7b31c..a81c275e20 100644 --- a/changelog.md +++ b/changelog.md @@ -3,8 +3,10 @@ __1.1.0__ - Service `setup` called before `app.io` instantiated ([#131](https://github.com/feathersjs/feathers/issues/131)) +- Allow to register services that already are event emitter ([#118](https://github.com/feathersjs/feathers/issues/118)) +- Clustering microservices ([#121](https://github.com/feathersjs/feathers/issues/121)) - Add debug module and messages ([#114](https://github.com/feathersjs/feathers/issues/114)) -- Server hardening with socket message validation and normalization ([#113](https://github.com/feathersjs/feathers/issues/113)) +- Server hardening with socket message validation and normalization ([#113](https://github.com/feathersjs/feathers/issues/113)) - Custom service events ([#111](https://github.com/feathersjs/feathers/issues/111)) - Support for registering services dynamically ([#67](https://github.com/feathersjs/feathers/issues/67), [#96](https://github.com/feathersjs/feathers/issues/96), [#107](https://github.com/feathersjs/feathers/issues/107)) diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 446ea363e8..0000000000 --- a/docs/api.md +++ /dev/null @@ -1,664 +0,0 @@ ---- -layout: page -title: API -permalink: /api/ -weight: 2 ---- - -## Configuration - -### REST - -In almost every case you want to expose your services through a RESTful JSON interface. This can be achieved by calling `app.configure(feathers.rest())`. Note that you will have to provide your own body parser middleware like the standard [Express 4 body-parser](https://github.com/expressjs/body-parser) to make REST `.create`, `.update` and `.patch` calls pass the parsed data. - -To set service parameters in a middleware, just attach it to the `req.feathers` object which will become the params for any service call. It is also possible to use URL parameters for REST API calls which will also be added to the params object: - -```js -var bodyParser = require('body-parser'); - -app.configure(feathers.rest()) - .use(bodyParser.json()) - .use(bodyParser.urlencoded({extended: true})) - .use(function(req, res, next) { - req.feathers.data = 'Hello world'; - next(); - }); - -app.use('/:app/todos', { - get: function(name, params, callback) { - console.log(params.data); // -> 'Hello world' - console.log(params.app); // will be `my` for GET /my/todos/dishes - callback(null, { - id: name, - params: params, - description: "You have to do " + name + "!" - }); - } -}); -``` - -The default REST handler is a middleware that formats the data retrieved by the service as JSON. If you would like to configure your own `handler` middleware just pass it to `feathers.rest(handler)`. For example, a middleware that just renders plain text with the todo description (`res.data` contains the data returned by the service): - -```js -app.configure(feathers.rest(function restFormatter(req, res) { - res.format({ - 'text/plain': function() { - res.end('The todo is: ' + res.data.description); - } - }); - })) - .use('/todo', { - get: function (id, params, callback) { - callback(null, { description: 'You have to do ' + id }); - } - }); -``` - -If you want to add other middleware *before* the REST handler, simply call `app.use(middleware)` before configuring the handler. - -### SocketIO - -To expose services via [SocketIO](http://socket.io/) call `app.configure(feathers.socketio())`. It is also possible pass a `function(io) {}` when initializing the provider where `io` is the main SocketIO object. Since Feathers is only using the SocketIO default configuration, this is a good spot to initialize the [recommended production settings](https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO#recommended-production-settings): - -```js -app.configure(feathers.socketio(function(io) { - io.enable('browser client minification'); // send minified client - io.enable('browser client etag'); // apply etag caching logic based on version number - io.enable('browser client gzip'); // gzip the file - - // enable all transports (optional if you want flashsocket support, please note that some hosting - // providers do not allow you to create servers that listen on a port different than 80 or their - // default port) - io.set('transports', [ - 'websocket' - , 'flashsocket' - , 'htmlfile' - , 'xhr-polling' - , 'jsonp-polling' - ]); -})); -``` - -> Note: io.set is deprecated in Socket.IO 1.0. The above configuration will still work but will be replaced with the recommended production configuration for version 1.0 (which isn't available at the moment). - -This is also the place to listen to custom events or add [authorization](https://github.com/LearnBoost/socket.io/wiki/Authorizing): - -```js -app.configure(feathers.socketio(function(io) { - io.on('connection', function(socket) { - socket.emit('news', { hello: 'world' }); - socket.on('my other event', function (data) { - console.log(data); - }); - }); - - io.use(function (socket, next) { - // Authorize using the /users service - app.service('users').find({ - username: socket.request.username, - password: socket.request.password - }, next); - }); -})); -``` - -Similar than the REST middleware, the SocketIO socket `feathers` property will be extended -for service parameters: - -```js -app.configure(feathers.socketio(function(io) { - io.use(function (socket, next) { - socket.feathers.user = { name: 'David' }; - next(); - }); -})); - -app.use('todos', { - create: function(data, params, callback) { - // When called via SocketIO: - params.user // -> { name: 'David' } - } -}); -``` - -Once the server has been started with `app.listen()` the SocketIO object is available as `app.io`. - -### Primus - -[Primus](https://github.com/primus/primus) is a universal wrapper for real-time frameworks and allows you to transparently use Engine.IO, WebSockets, BrowserChannel, SockJS and Socket.IO. Set it up with `feathers.primus(configuration [, fn])` where `configuration` is the [Primus server configuration](https://github.com/primus/primus#getting-started) and `fn` an optional callback with the Primus server instance that can e.g. be used for setting up [authorization](https://github.com/primus/primus#authorization): - -```js -// Set up Primus with SockJS -app.configure(feathers.primus({ - transformer: 'sockjs' -}, function(primus) { - // Set up Primus authorization here - primus.authorize(function (req, done) { - var auth; - - try { auth = authParser(req.headers['authorization']) } - catch (ex) { return done(ex) } - - // Do some async auth check - authCheck(auth, done); - }); -})); -``` - -In the Browser you can connect like this: - -```html - - -``` - -Just like REST and SocketIO, the Primus request object can be extended with a `feathers` parameter during authorization which will extend the `params` for any service request: - -```js -app.configure(feathers.primus({ - transformer: 'sockjs' -}, function(primus) { - // Set up Primus authorization here - primus.authorize(function (req, done) { - req.feathers = { - user: { name: 'David' } - } - - done(); - }); -})); -``` - -## API - -### listen - -`app.listen([port])` starts the application on the given port. It will first call the original [Express app.listen([port])](http://expressjs.com/api.html#app.listen), then run `app.setup(server)` (see below) with the server object and then return the server object. - -### setup - -`app.setup(server)` is used to initialize all services by calling each services `.setup(app, path)` method (if available). -It will also use the `server` instance passed (e.g. through `http.createServer`) to set up SocketIO (if enabled) and any other provider that might require the server instance. - -Normally `app.setup` will be called automatically when starting the application via `app.listen([port])` but there are cases when you need to initialize the server separately: - -__HTTPS__ - -With your Feathers application initialized it is easy to set up an HTTPS REST and SocketIO server: - -```js -app.configure(feathers.socketio()).use('/todos', todoService); - -var https = require('https'); -var server = https.createServer({ - key: fs.readFileSync('privatekey.pem'), - cert: fs.readFileSync('certificate.pem') -}, app).listen(443); - -// Call app.setup to initialize all services and SocketIO -app.setup(server); -``` - -__Virtual Hosts__ - -You can use the [vhost](https://github.com/expressjs/vhost) middleware to run your Feathers app on a virtual host: - -```js -var vhost = require('vhost') - -app.use('/todos', todoService); - -var host = feathers().use(vhost('foo.com', app)); -var server = host.listen(8080); - -// Here we need to call app.setup because .listen on our virtal hosted -// app is never called -app.setup(server); -``` - -### use - -`app.use([path], service)` works just like [Express app.use([path], middleware)](http://expressjs.com/api.html#app.use) but additionally allows to register a service object (an object which at least provides one of the service methods as outlined in the Services section) instead of the middleware function. Note that REST services are registered in the same order as any other middleware so the below example will allow the `/todos` service only to [Passport](http://passportjs.org/) authenticated users. - -```js -// Serve public folder for everybody -app.use(feathers.static(__dirname + '/public'); -// Make sure that everything else only works with authentication -app.use(function(req,res,next){ - if(req.isAuthenticated()){ - next(); - } else { - // 401 Not Authorized - next(new Error(401)); - } -}); -// Add a service. -app.use('/todos', { - get: function(name, params, callback) { - callback(null, { - id: name, - description: "You have to do " + name + "!" - }); - } -}); -``` - -### service - -`app.service(path [, service])` does two things. It either returns the Feathers wrapped service object for the given path or registers a new service for that path. - -`app.service(path)` returns the wrapped service object for the given path. Feathers internally creates a new object from each registered service. This means that the object returned by `service(path)` will provide the same methods and functionality as your original service object but also functionality added by Feathers and its plugins (most notably it is possible to listen to service events). `path` can be the service name with or without leading and trailing slashes. - -```js -app.use('/my/todos', { - create: function(data, params, callback) { - callback(null, data); - } -}); - -var todoService = app.service('my/todos'); -// todoService is an event emitter -todoService.on('created', function(todo) { - console.log('Created todo', todo); -}); -``` - -You can use `app.service(path, service)` instead `app.use(path, service)` if you want to be more explicit that you are registering a service. It is what is called internally by `app.use([path], service)` if a service object is being passed. `app.service` does __not__ provide the Express `app.use` functionality and does not check the service object for valid methods. - -## Services - -As mentioned, the basic Feathers functionality is fully compatible with Express. The key concept added to that of middleware is *service* objects. A service can be any JavaScript object that offers one or more of the `find`, `get`, `create`, `update`, `remove` and `setup` service methods with the following signatures: - -```js -var myService = { - find: function(params, callback) {}, - get: function(id, params, callback) {}, - create: function(data, params, callback) {}, - update: function(id, data, params, callback) {}, - patch: function(id, data, params, callback) {}, - remove: function(id, params, callback) {}, - setup: function(app) {} -} -``` - -And can be used like any other Express middleware `app.use('/my-service', myService)`. - -Although your service object must provide the full method signatures, `params` and `callback` are always optional when calling services (e.g. through websockets or when retrieving the wrapped service object with `app.service(path)`). If not provided the default `params` will be empty and `callback` will be an empty function. - -All service callbacks follow the `function(error, data)` NodeJS convention. `params` can contain any additional parameters, for example the currently authenticated user. REST service calls set `params.query` with the query parameters (e.g. a query string like `?status=active&type=user` becomes `{ query: { status: "active", type: "user" } }`), socket call parameters will also be passed as `params.query`. - -It is also possible to return a [Promise](http://promises-aplus.github.io/promises-spec/) object from a service instead of using the callback, for example using [Q](https://github.com/kriskowal/q): - -```js -var Q = require('q'); - -var todos = { - get: function(id) { - var dfd = Q.defer(); - - setTimeout(function() { - dfd.resolve({ - id: id, - description: 'You have to do ' + id - }); - }, 500); - - return dfd.promise; - } -} -``` - -### find - -`find(params, callback)` retrieves a list of all resources from the service. SocketIO parameters will be passed as `params.query` to the service. - -__REST__ - - GET todo?status=completed&user=10 - -__SocketIO__ - -```js -socket.emit('todo::find', { - status: 'completed' - user: 10 -}, function(error, data) { -}); -``` - -> Will call .create with `params` { query: { status: 'completed', user: 10 } } - -### get - -`get(id, params, callback)` retrieves a single resource with the given `id` from the service. - -__REST__ - - GET todo/1 - -__SocketIO__ - -```js -socket.emit('todo::get', 1, {}, function(error, data) { - -}); -``` - -### create - -`create(data, params, callback)` creates a new resource with `data`. The callback should be called with the newly -created resource data. - -__REST__ - - POST todo - { "description": "I really have to iron" } - -By default the body can be eihter JSON or form encoded as long as the content type is set accordingly. - -__SocketIO__ - -```js -socket.emit('todo::create', { - description: 'I really have to iron' -}, {}, function(error, data) { -}); -``` - -### update - -`update(id, data, params, callback)` updates the resource identified by `id` using `data`. The callback should -be called with the updated resource data. - -__REST__ - - PUT todo/2 - { "description": "I really have to do laundry" } - -__SocketIO__ - -```js -socket.emit('todo::update', 2, { - description: 'I really have to do laundry' -}, {}, function(error, data) { - // data -> { id: 2, description: "I really have to do laundry" } -}); -``` - -### patch - -`patch(id, data, params, callback)` patches the resource identified by `id` using `data`. The callback should be called with the updated resource data. Implement `patch` additionally to `update` if you want to separate between partial and full updates and support the `PATCH` HTTP method. - -__REST__ - - PATCH todo/2 - { "description": "I really have to do laundry" } - -__SocketIO__ - -```js -socket.emit('todo::patch', 2, { - description: 'I really have to do laundry' -}, {}, function(error, data) { - // data -> { id: 2, description: "I really have to do laundry" } -}); -``` - -### remove - -`remove(id, params, callback)` removes the resource with `id`. The callback should be called with the removed resource. - -__REST__ - - DELETE todo/2 - -__SocketIO__ - -```js -socket.emit('todo::remove', 2, {}, function(error, data) { -}); -``` - -### setup - -`setup(app, path)` initializes the service passing an instance of the Feathers application and the path it has been registered on. The SocketIO server is available via `app.io`. `setup` is a great way to connect services: - -```js -var todoService = { - get: function(name, params, callback) { - callback(null, { - id: name, - description: 'You have to ' + name + '!' - }); - } -}; - -var myService = { - setup: function(app) { - this.todo = app.service('todo'); - }, - - get: function(name, params, callback) { - this.todo.get('take out trash', {}, function(error, todo) { - callback(error, { - name: name, - todo: todo - }); - }); - } -} - -feathers() - .use('todo', todoService) - .use('my', myService) - .listen(8000); -``` - -You can see the combination when going to `http://localhost:8000/my/test`. - -__Pro tip:__ - -Bind the apps `service` method to your service to always look your services up dynamically: - -```js -var myService = { - setup: function(app) { - this.service = app.service.bind(app); - }, - - get: function(name, params, callback) { - this.service('todos').get('take out trash', {}, function(error, todo) { - callback(null, { - name: name, - todo: todo - }); - }); - } -} -``` - -## Events - -Any registered service will be automatically turned into an event emitter that emits events when a resource has changed, that is a `create`, `update` or `remove` service call returned successfully. It is therefore possible to bind to the below events via `app.service(servicename).on()` and, if enabled, all events will also broadcast to all connected SocketIO clients in the form of ` `. Note that the service path will always be stripped of leading and trailing slashes regardless of how it has been registered (e.g. `/my/service/` will become `my/service`). - -### created - -The `created` event will be published with the callback data when a service `create` returns successfully. - -```js -app.use('/todos', { - create: function(data, params, callback) { - callback(null, data); - } -}); - -app.service('/todos').on('created', function(todo) { - console.log('Created todo', todo); -}); - -app.service('/todos').create({ - description: 'We have to do something!' -}, {}, function(error, callback) { - // ... -}); - -app.listen(8000); -``` - -__SocketIO__ - -```html - - -``` - -### updated, patched - -The `updated` and `patched` events will be published with the callback data when a service `update` or `patch` method calls back successfully. - -```js -app.use('/my/todos/', { - update: function(id, data, params, callback) { - callback(null, data); - } -}); - -app.listen(8000); -``` - -__SocketIO__ - -```html - - -``` - -### removed - -The `removed` event will be published with the callback data when a service `remove` calls back successfully. - -```js -app.use('/todos', { - remove: function(id, params, callback) { - callback(null, { id: id }); - } -}); - -app.service('/todos').remove(1, {}, function(error, callback) { - // ... -}); - -app.listen(8000); -``` - -__SocketIO__ - -```html - - -``` - -### Event filtering - -By default all service events will be dispatched to all connected clients. -In many cases you probably want to be able to only dispatch events for certain clients. -This can be done by implementing the `created`, `updated`, `patched` and `removed` methods as `function(data, params, callback) {}` with `params` being the parameters set when the client connected, in SocketIO when authorizing and setting `socket.feathers` and Primus with `req.feathers`. - -```js -var myService = { - created: function(data, params, callback) {}, - updated: function(data, params, callback) {}, - patched: function(data, params, callback) {}, - removed: function(data, params, callback) {} -} -``` - -The event dispatching service methods will run for every connected client. Calling the callback with data (that you also may modify) will dispatch the according event. Callling back with a falsy value will prevent the event being dispatched to this client. - -The following example only dispatches the Todo `updated` event if the authorized user belongs to the same company: - -```js -app.configure(feathers.socketio(function(io) { - io.use(function (socket, callback) { - // Authorize using the /users service - app.service('users').find({ - username: socket.request.username, - password: socket.request.password - }, function(error, user) { - if(!error || !user) { - return callback(new Error('Not authenticated!')); - } - - socket.feathers = { - user: user - }; - - callback(); - }); - }); -})); - -app.use('todos', { - update: function(id, data, params, callback) { - // Update - callback(null, data); - }, - - updated: function(todo, params, callback) { - // params === socket.feathers - if(todo.companyId === params.user.companyId) { - // Dispatch the todo data to this client - return callback(null, todo); - } - - // Call back with a falsy value to prevent dispatching - callback(null, false); - } -}); -``` - -On the client: - -```js -socket.on('todo updated', function(data) { - // The client will only get this event - // if authorized and in the same company -}); -``` diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index de362fb0a4..0000000000 --- a/docs/faq.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -layout: page -title: FAQ -permalink: /faq/ -weight: 4 ---- - -## The Feathers FAQ - -On this page you can find a list of Feathers related questions that came up before. Make sure to also head over to the [Express FAQ](http://expressjs.com/faq.html). As already mentioned, since Feathers directly extends Express, everything there applies to Feathers as well. You are more than welcome to submit any questions as a [GitHub issue](https://github.com/feathersjs/feathers/issues) or on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. - -## Where can I get help? - -If you have any questions, feel free to submit them as a [GitHub issue](https://github.com/feathersjs/feathers/issues) or on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We are also on IRC in the [#feathersjs](http://webchat.freenode.net/?channels=feathersjs) channel on Freenode. - -## Why another Node web framework? - -We know! Oh God another NodeJS framework! We really didn't want to add another name to the long list of NodeJS web frameworks but also wanted to explore a different approach than any other library we have seen. We strongly believe that data is the core of the web and should be the focus of web applications. - -Many web frameworks end up focussing so much on secondary concerns like how to render views or handle and process HTTP requests and responses that even when using the MVC pattern your actual application logic becomes a slave to those concerns. - -Feathers services bring two important concepts together that help to separate those concerns from how your application works: - -1) A __[service layer](http://martinfowler.com/eaaCatalog/serviceLayer.html)__ which helps to decouple your application logic from how it is being accessed and represented. Besides also making things a lot easier to test - you just call your service methods instead of having to make fake HTTP requests - this is what allows Feathers to provide the same API through both HTTP REST and websockets. It can even be extended to use any other RPC protocol and you won't have to change any of your services. - -2) A __[uniform interface](http://en.wikipedia.org/wiki/Representational_state_transfer#Uniform_interface)__ which is one of the key constraints of [REST](http://en.wikipedia.org/wiki/Representational_state_transfer) in which context it is commonly referred to as the different HTTP verbs (GET, POST, PUT, PATCH and DELETE). This translates almost naturally into the Feathers service object interface: - -```js -var myService = { - // GET /path - find: function(params, callback) {}, - // GET /path/ - get: function(id, params, callback) {}, - // POST /path - create: function(data, params, callback) {}, - // POST /path/ - update: function(id, data, params, callback) {}, - // PATCH /path/ - patch: function(id, data, params, callback) {}, - // DELETE /patch/ - remove: function(id, params, callback) {} -} -``` - -This interface also made it easier to hook into the execution of those methods and emit events when they return. - -## Do I get websocket events from REST calls? - -Yes. Every service emits all events no matter from where it has been called. So even creating a new Todo internally on the server will send the event out on every socket that should receive it. This is very similar to what [Firebase](http://firebase.io/) does (but for free and open source). - -You can also listen to events on the server by retrieving the wrapped service object which is an event emitter: - -```js -// Retrieve the registered Todo service -var todoService = app.service('todos'); -var todoCount = 0; - -todoService.on('created', function(todo) { - // Increment the total number of created todos - todoCount++; -}); -``` - -## Is there a way to know where a method call came from? - -Sometimes you want to allow certain service calls internally (like creating a new user) but not through the REST or websocket API. This can be done by adding the information in a middleware to the `request.feathers` object which will be merged into service call parameters: - -```js -app.use(function(req, res, next) { - req.feathers.external = 'rest'; - next(); -}); - -app.configure(feathers.socketio(function(io) { - io.use(function(socket, next) { - // For websockets the feathers object does not exist by default - if(!socket.feathers) { - socket.feathers = {}; - } - - socket.feathers.external = 'socketio'; - next(); - }); -})); - - -app.use('/todos', { - get: function(id, params, callback) { - if(!params.external) { - return callback(null, { - id: id, - text: 'Do ' + id + '!' - }); - } - callback(new Error('External access not allowed')); - } -}); - -var todoService = app.service('todos'); -// Call .get without the external parameter set to get the result -todoService.get('laundry', {}, function(error, todo) { - todo.text // -> 'Do laundry!' -}); -``` - -## How do I add authentication? - -Generally any authentication mechanism used for Express can also be implemented in Feathers. - -Please refer to the [authentication](/#authentication) and [authorization](/#authorization) section of the guide and, in more detail, the [feathers-hooks](https://github.com/feathersjs/feathers-hooks) and [feahters-passport](https://github.com/feathersjs/feathers-passport) modules for more information. - -## Can I only send certain events? - -In almost any larger application not every user is supposed to receive every event through websockets. The [event filtering section](/api/#event-filtering) in the API documentation contains detailed documentation on how to only send events to authorized users. - -The following example only dispatches the Todo `updated` event if the authorized user belongs to the same company: - -```js -app.configure(feathers.socketio(function(io) { - io.use(function (socket, callback) { - // Authorize using the /users service - app.lookup('users').find({ - username: handshake.username, - password: handshake.password - }, function(error, user) { - if(!error || !user) { - return callback(error, false); - } - - socket.feathers = { - user: user - }; - - callback(null, true); - }); - }); -})); - -app.use('todos', { - update: function(id, data, params, callback) { - // Update - callback(null, data); - }, - - updated: function(todo, params, callback) { - // params === handshake.feathers - if(todo.companyId === params.user.companyId) { - // Dispatch the todo data to this client - return callback(null, todo); - } - - // Call back with a falsy value to prevent dispatching - callback(null, false); - } -}); -``` - -## Can I add custom middleware to a service? - -Custom Express middleware that only should be run before a specific service can simply be passed to `app.use` before the service object: - -```js -app.use('/todos', ensureAuthenticated, logRequest, todoService); -``` - -Keep in mind that shared authentication (between REST and websockets) should use a service based approach as described in the [authentication section of the guide](/#authentication). - -## What about Koa? - -Koa is a *"next generation web framework for Node.JS"* using ES6 generator functions instead of Express middleware. This approach does unfortunately not easily play well with Feathers services so there are no direct plans yet to use it as a future base for Feathers. - -There are however definitely plans of using ES6 features for Feathers once they make it into `node --harmony`, specifically: - -- [Promises](http://www.html5rocks.com/en/tutorials/es6/promises/) instead of callbacks for asynchronous processing -- [ES6 classes](http://wiki.ecmascript.org/doku.php?id=strawman:maximally_minimal_classes) for defining services. - -And a lot of the other syntactic sugar that comes with ES6 like arrow functions etc. If you want to join the discussion, chime in on [Feathers issue #83](https://github.com/feathersjs/feathers/issues/83) diff --git a/docs/guide.md b/docs/guide.md deleted file mode 100644 index 21eb51b0d7..0000000000 --- a/docs/guide.md +++ /dev/null @@ -1,630 +0,0 @@ ---- -layout: index -title: Guide -weight: 1 -permalink: / -anchor: guide ---- - -## To get started - -Feathers extends [Express 4](http://expressjs.com), one of the most popular web frameworks for [NodeJS](http://nodejs.org/). It makes it easy to create shared RESTful web services and real-time applications using SocketIO and several other NodeJS websocket libraries supported by [Primus](http://primus.io). - -If you are not familiar with Express head over to the [Express Guides](http://expressjs.com/guide.html) to get an idea. Feathers works the exact same way and supports the same functionality except that `var app = require('express')();` is replaced with `var app = require('feathers')()`. This means that you can literally drop Feathers into your existing Express 4 application and start adding new services right away. - -The following guide will walk through creating a basic Todo REST and websocket API with Feathers and MongoDB and also explain how to add authentication and authorization. For additional information also make sure to read through the [API documentation](/api/) and [FAQ](/faq/) later. - -To get started with this guide, lets create a new folder and in it run - -> `npm install feathers` - -## Your first service - -The most important concept Feathers adds to Express is that of *services*. Services can be used just like an Express middleware function but instead are JavaScript objects that provide at least one of the following methods: - -```js -var myService = { - find: function(params, callback) {}, - get: function(id, params, callback) {}, - create: function(data, params, callback) {}, - update: function(id, data, params, callback) {}, - patch: function(id, data, params, callback) {}, - remove: function(id, params, callback) {}, - setup: function(app, path) {} -} -``` - -This object can be registered like `app.use('/my-service', myService)` which - if configured - makes it available as a REST endpoint at `/my-service` and also through websockets. As usual in NodeJS, `callback` has to be called with the error (if any) first and the data as the second parameter. - -### Simple Todo - -With those methods available we can implement a very basic Todo service that returns a single Todo using the id passed to the `get` method: - -```js -// app.js -var feathers = require('feathers'); -var app = feathers(); -var todoService = { - get: function(id, params, callback) { - // Call back with no error and the Todo object - callback(null, { - id: id, - text: 'You have to do ' + id + '!' - }); - } -}; - -app.configure(feathers.rest()) - .use('/todos', todoService) - .listen(3000); -``` - -After running - -> `node app.js` - -You can go to [localhost:3000/todos/dishes](http://localhost:3000/todos/dishes) and should see the following JSON response: - -```js -{ - "id": "dishes", - "text": "You have to do dishes!" -} -``` - -### CRUD Todos - -As you might have noticed, service methods mainly reflect basic [CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) functionality. Following up is a longer example with comments for implementing a complete Todo service that manages all Todos in memory: - -```js -// todos.js -module.exports = { - // The current id counter - id: 0, - // An array with all todos - todos: [], - - // Tries to get a single Todo by its id. - // Throws an error if none can be found. - getTodo: function(id) { - var todos = this.todos; - - for(var i = 0; i < todos.length; i++) { - if(todos[i].id === parseInt(id, 10)) { - return todos[i]; - } - } - - // If we didn't return yet we can throw an error - throw new Error('Could not find Todo'); - }, - - // Return all Todos - find: function(params, callback) { - callback(null, this.todos); - }, - - // Returns a single Todo by id - get: function(id, params, callback) { - try { - callback(null, this.getTodo(id)); - } catch(error) { - callback(error); - } - }, - - // Create a new Todo - create: function(data, params, callback) { - // Increment the global ID counter and - // use it as the Todos id - data.id = this.id++; - this.todos.push(data); - callback(null, data); - }, - - // Update (replace) an existing Todo with new data - update: function(id, data, params, callback) { - try { - var todo = this.getTodo(id); - var index = this.todos.indexOf(todo); - - data.id = todo.id; - // Replace all the data - this.todos[index] = data; - callback(null, data); - } catch(error) { - callback(error); - } - }, - - // Extend the data of an existing Todo - patch: function(id, data, params, callback) { - try { - var todo = this.getTodo(id); - - // Extend the existing Todo with the new data - Object.keys(data).forEach(function(key) { - if(key !== 'id') { - todo[key] = data[key]; - } - }); - - callback(null, todo); - } catch(error) { - callback(error); - } - }, - - // Remove an existing Todo by id - remove: function(id, params, callback) { - try { - var todo = this.getTodo(id); - var index = this.todos.indexOf(todo); - - // Splice it out of our Todo list - this.todos.splice(index, 1); - callback(null, todo); - } catch(error) { - callback(error); - } - } -} -``` - -The above example exports the service as a module from its own file, `todos.js`. This means that in `app.js` we can replace the previous `todoService` with loading that module. In order to parse JSON encoded HTTP bodies we additionally need to install and load the Express [body-parser](https://github.com/expressjs/body-parser): - -> `npm install body-parser` - -```js -// app.js -var feathers = require('feathers'); -var bodyParser = require('body-parser'); - -var app = feathers(); -var todoService = require('./todos'); - -app.configure(feathers.rest()) - .use(bodyParser.json()) - .use('/todos', todoService) - .listen(3000); -``` - -Running `app.js` will now provide a fully functional REST API at `http://localhost:3000/todos`. You can test it, for example, using the [Postman](https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en) REST client plugin for Google chrome or via CURL: - - -
curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "You have to do dishes!" }'
- -## Getting real-time - -As previously mentioned, a Feathers service can also be exposed through websockets. You can either use [SocketIO](http://socket.io) or [Primus](https://github.com/primus/primus) - an abstraction layer for differentNode websocket libraries. In the following examples we will use SocketIO. - -SocketIO can be enabled by calling `app.configure(feathers.socketio())`. Once set up, it is possible to call service methods by emitting events like `::` on the socket and also receive events by listening to ` ` (*eventname* can be `created`, `updated`, `patched` or `removed`). To make it easier to test in a web page, lets also statically host the files in the current folder. `app.js` then looks like this: - -```js -// app.js -var feathers = require('feathers'); -var bodyParser = require('body-parser'); - -var app = feathers(); -var todoService = require('./todos'); - -app.configure(feathers.rest()) - .configure(feathers.socketio()) - .use(bodyParser.json()) - .use('/todos', todoService) - .use('/', feathers.static(__dirname)) - .listen(3000); -``` - -To test the connection, we can create an `index.html` file in the same folder. The example page will connect to SocketIO, create a new Todo and also log when any Todo has been created, updated or patched: - -```html - - - - Feathers SocketIO example - - -

A Feathers SocketIO example

-

-
-  
-  
-
-
-```
-
-After restarting, going directly to [localhost:3000](http://localhost:3000) with the console open will show what is happening on the HTML page. You can also see the newly created Todo at the REST endpoint [localhost:3000/todos](http://localhost:3000/todos). With the page open, creating a new  Todo via the REST API, for example
-
-
curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "Do something" }'
- -will also log `Someone created a new Todo`. This is how you can implement real-time functionality in any web page by using standardized websocket messages instead of having to make up your own. - -## Persisting to MongoDB - -Our CRUD Todo functionality implemented in the service is very common and doesn't have to be re-done from scratch every time. In fact, this is almost exactly what is being provided already in the [feathers-memory](https://github.com/feathersjs/feathers-memory) module. Luckily we don't have to stop at storing everything in-memory. For the popular NoSQL database [MongoDB](http://mongodb.org) , for example, there already is the [feathers-mongodb](https://github.com/feathersjs/feathers-mongodb) module and if you need more ORM-like functionality through [Mongoose](http://mongoosejs.com/) you can also use [feathers-mongoose](https://github.com/feathersjs/feathers-mongoose). - -> `npm install feathers-mongodb` - -With a MongoDB instance running locally, we can replace our `todoService` in `app.js` with a MongoDB storage on the `feathers-demo` database and the `todos` collection like this: - -```js -// app.js -var feathers = require('feathers'); -var mongodb = require('feathers-mongodb'); -var bodyParser = require('body-parser'); - -var app = feathers(); -var todoService = mongodb({ - db: 'feathers-demo', - collection: 'todos' -}); - -app.configure(feathers.rest()) - .configure(feathers.socketio()) - .use(bodyParser.json()) - .use('/todos', todoService) - .use('/', feathers.static(__dirname)) - .listen(3000); -``` - -And just like this we have a full REST and real-time Todo API that stores its data into MongoDB in just 16 lines of code! We will continue using MongoDB so we don't need our example `todos.js` service anymore. - -## Validation and processing - -The next step is validating and processing our data. With the MongoDB service already implemented we have two options to extend its functionality. - -### Service Extension - -*feathers-mongodb* uses the ES5 inheritance library [Uberproto](https://github.com/daffl/uberproto). This allows us to `extend` the original object returned by the call to `mongodb(options)` and overwrite the existing implementation of `create` to process the Todo data and then pass it to the original (`_super`) method. This way we can also easily add our own methods to the service. - -```js -var todoService = mongodb({ - db: 'feathers-demo', - collection: 'todos' -}).extend({ - create: function(data, params, callback) { - // We want to make sure that `complete` is always set - // and also only use the `text` and `complete` properties - var newData = { - text: data.text, - complete: data.complete === 'true' || !!data.complete - }; - // Call the original method with the new data - this._super(newData, params, callback); - }, - - // Add another method - addDefaultTodo: function(callback) { - this.create({ - text: 'The default todo', - complete: false - }, {}, callback); - } -}); -``` - -### Hooks - -Another option is the [feathers-hooks](https://github.com/feathersjs/feathers-hooks) plugin which allows us to add asynchronous hooks before or after a service method call. Hooks work similar to Express middleware. The following example adds a hook that converts our Todo data and makes sure that nobody submits anything that we don't want to put into MongoDB: - -> `npm install feathers-hooks` - -```js -// app.js -var feathers = require('feathers'); -var mongodb = require('feathers-mongodb'); -var hooks = require('feathers-hooks'); -var bodyParser = require('body-parser'); - -var app = feathers(); -var todoService = mongodb({ - db: 'feathers-demo', - collection: 'todos' -}); - -app.configure(feathers.rest()) - .configure(feathers.socketio()) - // Configure hooks - .configure(hooks()) - .use(bodyParser.json()) - .use('/todos', todoService) - .use('/', feathers.static(__dirname)) - .listen(3000); - -// Get the wrapped todos service object and -// add a `before` create hook modifying the data -app.service('todos').before({ - create: function(hook, next) { - var oldData = hook.data; - // Replace the old data by creating a new object - hook.data = { - text: oldData.text, - complete: oldData.complete === 'true' || !!oldData.complete - }; - next(); - } -}); -``` - -You might have noticed the call to [.service](/api/#toc9) in `app.service('todos')`. This will basically return the original service object (`todoService` in our case) *but* contain some functionality added by Feathers. Most notably, the returned service object will be an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) that emits `created`, `updated` etc. events. - -The *feathers-hooks* plugin also adds a `.before` and `.after` method that allows to add hooks to that service. When you need to access services, *always* use `app.service(name)` and not the original service object otherwise things will not work as expected. - -## Authentication - -Since Feathers directly extends Express you can use any of its authentication mechanism. [Passport](http://passportjs.org/) is one that is used quite often and also really flexible. Manually setting up shared authentication between websockets and an HTTP REST API can be tricky. This is what the [feathers-passport](https://github.com/feathersjs/feathers-passport) module aims to make easier. The following examples show how to add local authentication that uses a Feathers service for storing and retrieving user information. - -### Configuring Passport - -The first step is to add the Passport, local strategy and feathers-passport modules to our application. Since we are using MongoDB already we will also use it as the session store through the [connect-mongo](https://github.com/kcbanner/connect-mongo) module: - -> `npm install passport passport-local connect-mongo feathers-passport` - -```js -// app.js -var feathers = require('feathers'); -var mongodb = require('feathers-mongodb'); -var bodyParser = require('body-parser'); -var hooks = require('feathers-hooks'); - -var passport = require('passport'); -var connectMongo = require('connect-mongo'); -var feathersPassport = require('feathers-passport'); - -var app = feathers(); -var todoService = mongodb({ - db: 'feathers-demo', - collection: 'todos' -}); - -app.configure(feathers.rest()) - .configure(feathers.socketio()) - .configure(hooks()) - .configure(feathersPassport(function(result) { - // MongoStore needs the session function - var MongoStore = connectMongo(result.createSession); - - result.secret = 'feathers-rocks'; - result.store = new MongoStore({ - db: 'feathers-demo' - }); - - return result; - })) - .use(bodyParser.json()) - // Now we also need to parse HTML form submissions - .use(bodyParser.urlencoded({ extended: true })) - .use('/todos', todoService) - .use('/', feathers.static(__dirname)); -``` - -### User storage - -Next, we create a MongoDB service for storing user information. It is always a good idea to not store plain text passwords in the database so we add a `.before` hook that salts and then hashes the password when creating a new user. This can be done in the service `.setup` which is called when the application is ready to start up. We also add an `.authenticate` method that we can use to look up a user by username and compare the hashed and salted passwords. - -```js -var crypto = require('crypto'); -// One-way hashes a string -var hash = function(string, salt) { - var shasum = crypto.createHash('sha256'); - shasum.update(string + salt); - return shasum.digest('hex'); -}; - -var userService = mongodb({ - db: 'feathers-demo', - collection: 'users' -}).extend({ - authenticate: function(username, password, callback) { - // This will be used as the MongoDB query - var query = { - username: username - }; - - this.find({ query: query }, function(error, users) { - if(error) { - return callback(error); - } - - var user = users[0]; - - if(!user) { - return callback(new Error('No user found')); - } - - // Compare the hashed and salted passwords - if(user.password !== hash(password, user.salt)) { - return callback(new Error('User password does not match')); - } - - // If we got to here, we call the callback with the user information - return callback(null, user); - }); - }, - - setup: function() { - // Adds the hook during service setup - this.before({ - // Hash the password before sending it to MongoDB - create: function(hook, next) { - // Create a random salt string - var salt = crypto.randomBytes(128).toString('base64'); - // Change the password to a hashed and salted password - hook.data.password = hash(hook.data.password, salt); - // Add the salt to the user data - hook.data.salt = salt; - next(); - } - }); - } -}); - -app.use('/users', userService); -``` - -Now we need to set up Passport to use that service and tell it how to deserialize and serialize our user information. For us, the serialized form is the `_id` generated by MongoDB. To deserialize by `_id` we can simply call the user services `.get` method. Then we add the local strategy which simply calls the `.authenticate` method that we implemented in the user service. - -```js -var LocalStrategy = require('passport-local').Strategy; - -passport.serializeUser(function(user, done) { - // Use the `_id` property to serialize the user - done(null, user._id); -}); - -passport.deserializeUser(function(id, done) { - // Get the user information from the service - app.service('users').get(id, {}, done); -}); - -passport.use(new LocalStrategy(function(username, password, done) { - app.service('users').authenticate(username, password, done); -})); -``` - -### Login - -The last step is to add the authentication route that we can POST the login to: - -```js -app.post('/login', passport.authenticate('local', { - successRedirect: '/', - failureRedirect: '/login.html', - failureFlash: false -})); - -app.listen(3000); -``` - -And to add a `login.html` page: - -```html - - - - - - - -
-
- - -
-
- - -
-
- -
-
- - -``` - -To test the login, we might want to add a new user as well: - -
curl 'http://localhost:3000/users/' -H 'Content-Type: application/json' --data-binary '{ "username": "feathers", "password": "supersecret" }'
- -Now it should be possible to log in with the `feathers` username and `supersecret` password and you will get the logged in user information in every service call in `params.user`. - -## Authorization - -Authorization is the process of determining after successful authentication if the user is allowed to perform the requested action. This is again where hooks come in handy. - -### User authorization - -Since *feathers-passport* adds the authenticated user information to the service call parameters we can just check those in the hook and return with an error if the user is not authorized: - -```js -app.service('todos').before({ - create: function(hook, next) { - // We only allow creating todos with an authenticated user - if(!hook.params.user) { - return next(new Error('You need to be authenticated')); - } - - // Check if the user belongs the `admin` group - var groups = hook.params.user.groups; - if(groups.indexOf('admin') === -1) { - // Return with an error if not - return next(new Error('User is not allowed to create a new Todo')); - } - - // Otherwise just continue on to the - // next hook or the service method - next(); - } -}); -``` - -### Event filtering - -This is also a good time to talk a little about [filtering events](/api/#event-filtering). It is very likely that you eventually only want to send certain events to specific users instead of everybody. Following up on the group authorization example from above, we might only want to dispatch a `todos created` event to users that are in the admin group. This can be done by adding a `created(data, params, callback)` method to the Todo MongoDB service: - -```js -var todoService = mongodb({ - db: 'feathers-demo', - collection: 'todos' -}).extend({ - created: function(data, params, callback) { - // Only dispatch if we have a user and user belongs to the admin group - if(params.user && params.user.groups.indexOf('admin') !== -1) { - // Call back with the data we want to dispatch - return callback(null, data); - } - - // Call back with falsy value to not dispatch the event - callback(null, false); - } -}); -``` - -The `created` method is being called for every connected user with the `params` set in the `request.feathers` object and the data from the event. You can either call back with the original or modified data (which will then be dispatched to that user) or a falsy value which will prevent the event from being dispatched to that connection. - -## What's next? - -This guide hopefully gave you an overview of how Feathers works. We created a Todo service and made it available through a REST API and SocketIO. Then we moved to using MongoDB as the backend storage and learned how to process and validate our data. After that we added PassportJS authentication for both, the REST API and websockets and then briefly discussed how you might authorize that authenticated user and make sure that websocket events only get dispatched to where we want them to. - -The next step is definitely reading through the [API documentation](/api/) for a more detailed information on how to configure and use certain parts of Feathers. The [FAQ](/faq/) also has some answers to questions that come up regularly. For a growing list of official plugins, have a look at the [Feathersjs GitHub organization](https://github.com/feathersjs). - -If you have any other questions, feel free to submit them as a [GitHub issue](https://github.com/feathersjs/feathers/issues) or on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag or join [#feathersjs](http://webchat.freenode.net/?channels=feathersjs) on Freenode IRC. diff --git a/package.json b/package.json index b485097740..8fde88a509 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "feathers", - "description": "Shared REST and real-time APIs with Express.", + "description": "Build Better APIs, Faster than Ever.", "version": "1.1.0-pre.0", "homepage": "http://feathersjs.com", "repository": { @@ -40,7 +40,6 @@ "npm": ">= 1.3.0" }, "dependencies": { - "body-parser": "^1.0.2", "debug": "^2.1.1", "express": "^4.12.3", "feathers-commons": "^0.2.0", diff --git a/readme.md b/readme.md index ac92e6e9f7..f68beb73a1 100644 --- a/readme.md +++ b/readme.md @@ -1,269 +1,18 @@ -# Feathers - Let your applications fly! +# Feathers - Build Better APIs, Faster than Ever [![Join the chat at https://gitter.im/feathersjs/feathers](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/feathersjs/feathers?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -> Shared REST and real-time APIs with Express. +[![Build Status](https://travis-ci.org/feathersjs/feathers-client.png?branch=master)](https://travis-ci.org/feathersjs/feathers-client) -## To get started +Feathers is a real-time, micro-service web framework for NodeJS that gives you control over your data via RESTful resources, sockets and flexible plug-ins. -Feathers extends [Express 4](http://expressjs.com), one of the most popular web frameworks for [NodeJS](http://nodejs.org/). It makes it easy to create shared RESTful web services and real-time applications using SocketIO and several other NodeJS websocket libraries. +## Getting started -If you are not familiar with Express head over to the [Express Guides](http://expressjs.com/guide.html) to get an idea. Feathers works the exact same way except that `var app = require('express')();` is replaced with `var app = require('feathers')()`. This means that you can literally drop Feathers into your existing Express 4 application and start adding new services right away. The following guide will walk through creating a basic Todo REST and websocket API with Feathers and MongoDB and also explain how to add authentication and authorization. For additional information also make sure to read through the [API documentation](/api/) later. +Visit the website at [feathersjs.com](http://feathersjs.com) to read the [Getting started guide](http://feathersjs.com/quick-start/) or learn how to build real-time applications with jQuery, Angular, React, CanJS, iOS, Android - you name it - and Feathers as the backend [in our guides](http://feathersjs.com/learn/). -To get started with this guide, lets create a new folder and in it +## A MongoDB REST and real-time API -> `npm install feathers` - -## Your first service - -The most important concept Feathers adds to Express is that of __services__. Services can be used just like an Express middleware function but instead are JavaScript objects that provide at least one of the following methods: - -```js -var myService = { - find: function(params, callback) {}, - get: function(id, params, callback) {}, - create: function(data, params, callback) {}, - update: function(id, data, params, callback) {}, - patch: function(id, data, params, callback) {}, - remove: function(id, params, callback) {}, - setup: function(app, path) {} -} -``` - -This object can be registered like `app.use('/my-service', myService)` which - if configured - makes it available as a REST endpoint at `/my-service` and also through websockets. As usual in NodeJS, `callback` has to be called with the error (if any) first and the data as the second parameter. - -### Simple Todo - -With those methods available we can implement a very basic Todo service that returns a single Todo using the id passed to the `get` method: - -```js -// app.js -var feathers = require('feathers'); -var app = feathers(); -var todoService = { - get: function(id, params, callback) { - // Call back with no error and the Todo object - callback(null, { - id: id, - text: 'You have to do ' + id + '!' - }); - } -}; - -app.configure(feathers.rest()) - .use('/todos', todoService) - .listen(3000); -``` - -After running - -> `node app.js` - -You can go to [localhost:3000/todos/dishes](http://localhost:3000/todos/dishes) and should see the following JSON response: - -```js -{ - "id": "dishes", - "text": "You have to do dishes!" -} -``` - -### CRUD Todos - -As you might have noticed, service methods mainly reflect basic [CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) functionality. Following up is a longer example with comments for implementing a complete Todo service that manages all Todos in memory: - -```js -// todos.js -module.exports = { - // The current id counter - id: 0, - // An array with all todos - todos: [], - - // Tries to get a single Todo by its id. - // Throws an error if none can be found. - getTodo: function(id) { - var todos = this.todos; - - for(var i = 0; i < todos.length; i++) { - if(todos[i].id === parseInt(id, 10)) { - return todos[i]; - } - } - - // If we didn't return yet we can throw an error - throw new Error('Could not find Todo'); - }, - - // Return all Todos - find: function(params, callback) { - callback(null, this.todos); - }, - - // Returns a single Todo by id - get: function(id, params, callback) { - try { - callback(null, this.getTodo(id)); - } catch(error) { - callback(error); - } - }, - - // Create a new Todo - create: function(data, params, callback) { - // Increment the global ID counter and - // use it as the Todos id - data.id = this.id++; - this.todos.push(data); - callback(null, data); - }, - - // Update (replace) an existing Todo with new data - update: function(id, data, params, callback) { - try { - var todo = this.getTodo(id); - var index = this.todos.indexOf(todo); - - data.id = todo.id; - // Replace all the data - this.todos[index] = data; - callback(null, data); - } catch(error) { - callback(error); - } - }, - - // Extend the data of an existing Todo - patch: function(id, data, params, callback) { - try { - var todo = this.getTodo(id); - - // Extend the existing Todo with the new data - Object.keys(data).forEach(function(key) { - if(key !== 'id') { - todo[key] = data[key]; - } - }); - - callback(null, todo); - } catch(error) { - callback(error); - } - }, - - // Remove an existing Todo by id - remove: function(id, params, callback) { - try { - var todo = this.getTodo(id); - var index = this.todos.indexOf(todo); - - // Splice it out of our Todo list - this.todos.splice(index, 1); - callback(null, todo); - } catch(error) { - callback(error); - } - } -} -``` - -The above example exports the service as a module from its own file, `todos.js`. This means that in `app.js` we can replace the previous `todoService` with loading that module. In order to parse JSON encoded HTTP bodies we additionally need to install and load the Express [body-parser](https://github.com/expressjs/body-parser): - -> `npm install body-parser` - -```js -// app.js -var feathers = require('feathers'); -var bodyParser = require('body-parser'); - -var app = feathers(); -var todoService = require('./todos'); - -app.configure(feathers.rest()) - .use(bodyParser.json()) - .use('/todos', todoService) - .listen(3000); -``` - -Running `app.js` will now provide a fully functional REST API at `http://localhost:3000/todos`. You can test it, for example, using the [Postman](https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en) REST client plugin for Google chrome or via CURL: - - -
curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "You have to do dishes!" }'
- -## Getting real-time - -As previously mentioned, a Feathers service can also be exposed through websockets. You can either use [SocketIO](http://socket.io) or [Primus](https://github.com/primus/primus) - an abstraction layer for differentNode websocket libraries. In the following examples we will use SocketIO. - -SocketIO can be enabled by calling `app.configure(feathers.socketio())`. Once set up, it is possible to call service methods by emitting events like `::` on the socket and also receive events by listening to ` ` (*eventname* can be `created`, `updated`, `patched` or `removed`). To make it easier to test in a web page, lets also statically host the files in the current folder. `app.js` then looks like this: - -```js -// app.js -var feathers = require('feathers'); -var bodyParser = require('body-parser'); - -var app = feathers(); -var todoService = require('./todos'); - -app.configure(feathers.rest()) - .configure(feathers.socketio()) - .use(bodyParser.json()) - .use('/todos', todoService) - .use('/', feathers.static(__dirname)) - .listen(3000); -``` - -To test the connection, we can create an `index.html` file in the same folder. The example will connect to SocketIO, create a new Todo and also log when any Todo has been created, updated or patched: - -```html - - - - Feathers SocketIO example - - -

A Feathers SocketIO example

- - - - -``` - -After restarting, going directly to [localhost:3000](http://localhost:3000) with the console open will show what is happening on the HTML page. You can also see the newly created Todo at the REST endpoint [localhost:3000/todos](http://localhost:3000/todos). With the page open, reating a new Todo via the REST API, for example - -
curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "Do something" }'
- -will also log `Someone created a new Todo`. This is how you can implement real-time functionality in any web page without a lot of magic using standardized websocket messages instead of having to re-invent your own. - -## Persisting to MongoDB - -Our CRUD Todo functionality implemented in the service is very common and doesn't have to be implemented form scratch every time. In fact, this is almost exactly what is being provided already in the [feathers-memory](https://github.com/feathersjs/feathers-memory) module. Luckily we don't have to stop at storing everything in-memory. For the popular NoSQL database [MongoDB](http://mongodb.org) , for example, there already is the [feathers-mongodb](https://github.com/feathersjs/feathers-mongodb) module and if you need more ORM-like functionality through [Mongoose](http://mongoosejs.com/) you can also use [feathers-mongoose](https://github.com/feathersjs/feathers-mongoose). - -> `npm install feathers-mongodb` - -With a MongoDB instance running locally, we can replace our `todoService` in `app.js` with a MongoDB storage on the `feathers-demo` database and the `todos` collection like this: +Curious how it looks? Here is a full REST and real-time todo API that uses MongoDB: ```js // app.js @@ -285,8 +34,11 @@ app.configure(feathers.rest()) .listen(3000); ``` -And just like this we have a full REST and real-time Todo API that stores its data into MongoDB in just 16 lines of code! +Then run -## Next steps +``` +npm install feathers feathers-mongodb body-parser +node app +``` -To learn more about Feathers go to the [feathersjs.com](http://feathersjs.com) homepage and continue reading this guide. +and go to [http://localhost:3000/todos](http://localhost:3000/todos). Don't want to use MongoDB? Feathers has plugins for [many other databases](http://feathersjs.com/learn/) and you can easily [write your own adapters](http://feathersjs.com/quick-start/). From 230917401077f7608a6c20bf00a49721cdcd7b30 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 21 Jul 2015 19:33:38 -0600 Subject: [PATCH 3/5] Better readme header. Adding body-parser back but as devDependency. --- package.json | 1 + readme.md | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8fde88a509..6d576151a1 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "uberproto": "^1.1.0" }, "devDependencies": { + "body-parser": "^1.13.2", "feathers-client": "^0.1.1", "jshint": "^2.6.3", "mocha": "^2.2.0", diff --git a/readme.md b/readme.md index f68beb73a1..e7a969ded4 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,11 @@ -# Feathers - Build Better APIs, Faster than Ever +
+ Feathers logo +

Build Better APIs, Faster than Ever

+
[![Join the chat at https://gitter.im/feathersjs/feathers](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/feathersjs/feathers?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://travis-ci.org/feathersjs/feathers-client.png?branch=master)](https://travis-ci.org/feathersjs/feathers-client) +[![Build Status](https://travis-ci.org/feathersjs/feathers.png?branch=master)](https://travis-ci.org/feathersjs/feathers) Feathers is a real-time, micro-service web framework for NodeJS that gives you control over your data via RESTful resources, sockets and flexible plug-ins. From eb9d97acc859ce448ad084f7dced324d4a2ca71d Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 21 Jul 2015 19:36:27 -0600 Subject: [PATCH 4/5] Use new TravisCI infrastructure. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a6601e45e0..73559fdc11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: node_js node_js: - "0.10" From fc6a46b59afbf4b82e48f10bf54a6c7899fe93d7 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 21 Jul 2015 19:37:48 -0600 Subject: [PATCH 5/5] 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d576151a1..5cc64ecdd6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "feathers", "description": "Build Better APIs, Faster than Ever.", - "version": "1.1.0-pre.0", + "version": "1.1.0", "homepage": "http://feathersjs.com", "repository": { "type": "git",