Feathers v3 (Buzzard)

Feathers v3 comes with some great improvements and new features and we highly recommend to upgrade as soon as possible. It might look a little daunting at first but in almost every case, the new CLI will get you almost all the way there automatically. This page contains information about the quick upgrade path and more information about all the changes to upgrade from Feathers v2 to v3.

Read the release post at Flying into 2018

Quick upgrade

To quickly upgrade any Feathers plugin or application you can use the upgrade command from the new CLI. First, if you have it installed, uninstall the old feathers-cli:

  1. npm uninstall feathers-cli -g

Then install @feathersjs/cli and upgrade a project:

  1. npm install @feathersjs/cli -g
  2. cd path/to/project
  3. feathers upgrade

The CLI will use the directories.lib in your package.json to know where your source files are located, defaulting to src if none provided. If you have a transpiled app/module, e.g. with babel, including a lib AND a src folder, then the most simple is to change the directories.lib in your package.json to srcinstead of lib so that the CLI will correctly upgrade the original source files and not the transpiled ones.

In short (for more details see below) this will:

  • Upgrade all core packages to the new scoped package names and their latest versions
  • Remove all feathers-hooks imports and single line app.configure(hooks()); (chained .configure(hooks()) calls will have to be removed manually))
  • Add Express compatibility to any application that uses feathers-rest (other Feathers apps without feathers-rest have to be updated manually)
  • Remove all .filter imports and calls to service.filter which has been replaced by channel functionality

Adding channels

If you are using real-time (with Socket.io or Primus), add the following file as src/channels.js:

  1. module.exports = function(app) {
  2. if(typeof app.channel !== 'function') {
  3. // If no real-time functionality has been configured just return
  4. return;
  5. }
  6. app.on('connection', connection => {
  7. // On a new real-time connection, add it to the anonymous channel
  8. app.channel('anonymous').join(connection);
  9. });
  10. app.on('login', (authResult, { connection }) => {
  11. // connection can be undefined if there is no
  12. // real-time connection, e.g. when logging in via REST
  13. if(connection) {
  14. // Obtain the logged in user from the connection
  15. // const user = connection.user;
  16. // The connection is no longer anonymous, remove it
  17. app.channel('anonymous').leave(connection);
  18. // Add it to the authenticated user channel
  19. app.channel('authenticated').join(connection);
  20. // Channels can be named anything and joined on any condition
  21. // E.g. to send real-time events only to admins use
  22. // if(user.isAdmin) { app.channel('admins').join(connection); }
  23. // If the user has joined e.g. chat rooms
  24. // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel));
  25. // Easily organize users by email and userid for things like messaging
  26. // app.channel(`emails/${user.email}`).join(channel);
  27. // app.channel(`userIds/$(user.id}`).join(channel);
  28. }
  29. });
  30. app.publish((data, hook) => { // eslint-disable-line no-unused-vars
  31. // Here you can add event publishers to channels set up in `channels.js`
  32. // To publish only for a specific event use `app.publish(eventname, () => {})`
  33. // e.g. to publish all service events to all authenticated users use
  34. return app.channel('authenticated');
  35. });
  36. // Here you can also add service specific event publishers
  37. // e..g the publish the `users` service `created` event to the `admins` channel
  38. // app.service('users').publish('created', () => app.channel('admins'));
  39. // With the userid and email organization from above you can easily select involved users
  40. // app.service('messages').publish(() => {
  41. // return [
  42. // app.channel(`userIds/${data.createdBy}`),
  43. // app.channel(`emails/${data.recipientEmail}`)
  44. // ];
  45. // });
  46. };

And require and configure it in src/app.js (note that it should be configured after all services so that channels.js can register service specific publishers):

  1. const channels = require('./channels');
  2. // After `app.configure(services)`
  3. app.configure(channels);

Very important: The channels.js file shown above will publish all real-time events to all authenticated users. This is already safer than the previous default but you should carefully review the channels documentation and implement appropriate channels so that only the right users are going to receive real-time events.

Once you migrated your application to channels you can remove all <servicename>.filter.js files.

Protecting fields

Feathers v3 has a new mechanism to ensure that sensitive information never gets published to any client. To protect always protect the user password, add the protect hook in src/services/users/users.hooks.js instead of the remove('password') hook:

  1. const { hashPassword } = require('@feathersjs/authentication-local').hooks;
  2. const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks;
  3. module.exports = {
  4. before: {
  5. all: [],
  6. find: [ authenticate('jwt') ],
  7. get: [],
  8. create: [],
  9. update: [],
  10. patch: [],
  11. remove: []
  12. },
  13. after: {
  14. all: [
  15. // Make sure the password field is never sent to the client
  16. // Always must be the last hook
  17. protect('password')
  18. ],
  19. find: [],
  20. get: [],
  21. create: [],
  22. update: [],
  23. patch: [],
  24. remove: []
  25. },
  26. error: {
  27. all: [],
  28. find: [],
  29. get: [],
  30. create: [],
  31. update: [],
  32. patch: [],
  33. remove: []
  34. }
  35. };

Updating client side applications

Client side Feathers applications can also be updated using the CLI but may need some manual intervention. Most importantly, since Feathers core now natively ships as ES6 code, the module bundler, like Webpack, has to be instructed to transpile it. More information can be found in the client chapter. For Webpack and create-react-app usage (which both will throw a minification error without changes), see this section.

@feathersjs npm scope

All Feathers core modules have been moved to the @feathersjs npm scope. This makes it more clear which modules are considered core and which modules are community supported and also allows us to more easily manage publishing permissions. The following modules have been renamed:

Main Feathers

Old name Scoped name
feathers @feathersjs/feathers
feathers-cli @feathersjs/cli
feathers-commons @feathersjs/commons
feathers-rest @feathersjs/express/rest
feathers-socketio @feathersjs/socketio
feathers-primus @feathersjs/primus
feathers-errors @feathersjs/errors
feathers-configuration @feathersjs/configuration
feathers-socket-commons @feathersjs/socket-commons

Authentication

Old name Scoped name
feathers-authentication @feathersjs/authentication
feathers-authentication-jwt @feathersjs/authentication-jwt
feathers-authentication-local @feathersjs/authentication-local
feathers-authentication-oauth1 @feathersjs/authentication-oauth1
feathers-authentication-oauth2 @feathersjs/authentication-oauth2
feathers-authentication-client @feathersjs/authentication-client

Client side Feathers

Old name Scoped name
feathers/client @feathersjs/feathers
feathers-client @feathersjs/client
feathers-rest/client @feathersjs/rest-client
feathers-socketio/client @feathersjs/socketio-client
feathers-primus/client @feathersjs/primus-client
feathers-authentication/client @feathersjs/authentication-client

Documentation changes

With a better focus on Feathers core, the repositories, documentation and guides for non-core module have been moved to more appropriate locations:

Framework independent

@feathersjs/feathers v3 is framework independent and will work on the client and in Node out of the box. This means that it is not extending Express by default anymore.

Instead @feathersjs/express provides the framework bindings and the REST provider (previously feathers-rest) in either require('@feathersjs/express').rest or @feathersjs/express/rest. @feathersjs/express also brings Express built-in middleware like express.static and the recently included express.json and express.urlencoded body parsers. Once a Feathers application is “expressified” it can be used like the previous version:

Before

  1. const feathers = require('feathers');
  2. const bodyParser = require('body-parser');
  3. const rest = require('feathers-rest');
  4. const errorHandler = require('feathers-errors/handler');
  5. const app = feathers();
  6. app.configure(rest());
  7. app.use(bodyParser.json());
  8. app.use(bodyParser.urlencoded({ extended: true }));
  9. // Register an Express middleware
  10. app.get('/somewhere', function(req, res) {
  11. res.json({ message: 'Data from somewhere middleware' });
  12. });
  13. // Statically host some files
  14. app.use('/', feathers.static(__dirname));
  15. // Use a Feathers friendly Express error handler
  16. app.use(errorHandler());

Now

  1. const feathers = require('@feathersjs/feathers');
  2. const express = require('@feathersjs/express');
  3. // Create an Express compatible Feathers application
  4. const app = express(feathers());
  5. // Add body parsing middleware
  6. app.use(express.json());
  7. app.use(express.urlencoded({ extended: true }));
  8. // Initialize REST provider (previous in `feathers-rest`)
  9. app.configure(express.rest());
  10. // Register an Express middleware
  11. app.get('/somewhere', function(req, res) {
  12. res.json({ message: 'Data from somewhere middleware' });
  13. });
  14. // Statically host some files
  15. app.use('/', express.static(__dirname));
  16. // Use a Feathers friendly Express error handler
  17. app.use(express.errorHandler());

Hooks in core

The feathers-hooks plugin is now a part of core and no longer has to be imported and configured. All services will have hook functionality included right away. Additionally it is now also possible to define different data that should be sent to the client in hook.dispatch which allows to properly secure properties that should not be shown to a client.

Before

  1. const feathers = require('feathers');
  2. const hooks = require('feathers-hooks');
  3. const app = feathers();
  4. app.configure(hooks());
  5. app.use('/todos', {
  6. get(id) {
  7. return Promise.resolve({
  8. message: `You have to do ${id}`
  9. });
  10. }
  11. });
  12. app.service('todos').hooks({
  13. after: {
  14. get(hook) {
  15. hook.result.message = `${hook.result.message}!`;
  16. }
  17. }
  18. });

Now

  1. const feathers = require('feathers');
  2. const app = feathers();
  3. app.use('/todos', {
  4. get(id) {
  5. return Promise.resolve({
  6. message: `You have to do ${id}`
  7. });
  8. }
  9. });
  10. app.service('todos').hooks({
  11. after: {
  12. get(hook) {
  13. hook.result.message = `${hook.result.message}!`;
  14. }
  15. }
  16. });

Event channels and publishing

Previously, filters were used to run for every event and every connection to determine if the event should be sent or not.

Event channels are a more secure and performant way to define which connections to send real-time events to. Instead of running for every event and every connection you define once which channels a connection belongs to when it is established or authenticated.

  1. // On login and if it is a real-time connectionn, add the connection to the `authenticated` channel
  2. app.on('login', (authResult, { connection }) => {
  3. if(connection) {
  4. const { user } = connection;
  5. app.channel('authenticated').join(connection);
  6. }
  7. });
  8. // Publish only `created` events from the `messages` service
  9. app.service('messages').publish('created', (data, context) => app.channel('authenticated'));
  10. // Publish all real-time events from all services to the authenticated channel
  11. app.publish((data, context) => app.channel('authenticated'));

To only publish to rooms a user is in:

  1. // On login and if it is a real-time connection, add the connection to the `authenticated` channel
  2. app.on('login', (authResult, { connection }) => {
  3. if(connection) {
  4. const { user } = connection;
  5. // Join `authenticated` channel
  6. app.channel('authenticated').join(connection);
  7. // Join rooms channels for that user
  8. rooms.forEach(roomId => app.channel(`rooms/${roomId}`).join(connection));
  9. }
  10. });

Better separation of client and server side modules

Feathers core was working on the client and the server since v2 but it wasn’t always entirely clear which related modules should be used how. Now all client side connectors are located in their own repositories while the main Feathers repository can be required the same way on the client and the server.

Before

  1. const io = require('socket.io-client');
  2. const feathers = require('feathers/client');
  3. const hooks = require('feathers-hooks');
  4. const socketio = require('feathers-socketio/client');
  5. const auth = require('feathers-authentication-client');
  6. const socket = io();
  7. const app = feathers()
  8. .configure(hooks())
  9. .configure(socketio(socket))
  10. .configure(auth());

Now

  1. const io = require('socket.io-client');
  2. const feathers = require('@feathersjs/feathers');
  3. const socketio = require('@feathersjs/socketio-client');
  4. const auth = require('@feathersjs/authentication-client');
  5. const socket = io();
  6. const app = feathers()
  7. .configure(socketio(socket))
  8. .configure(auth());

Node 6+

The core repositories mentioned above also have been migrated to be directly usable (e.g. when npm installing the repository as a Git/GitHub dependency) without requiring a Babel transpilation step.

Since all repositories make extensive use of ES6 that also means that Node 4 is no longer supported.

Also see /feathers/issues/608.

A new Socket message format

The websocket messaging format has been updated to support proper error messages when trying to call a non-existing service or method (instead of just timing out). Using the new @feathersjs/socketio-client and @feathersjs/primus-client will automatically use that format. You can find the details in the Socket.io client and Primus client documentation.

Note: The old message format is still supported so the clients do not have to be updated at the same time.

Deprecations and other API changes

  • Callbacks are no longer supported in Feathers service methods. All service methods always return a Promise. Custom services must return a Promise or use async/await.
  • service.before and service.after have been replaced with a single app.hooks({ before, after })
  • app.service(path) only returns a service and cannot be used to register a new service anymore (via app.service(path, service)). Use app.use(path, service) instead.
  • Route parameters which were previously added directly to params are now in params.route
  • Express middleware like feathers.static is now located in const express = require('@feathersjs/express') using express.static
  • Experimental TypeScript definitions have been removed from all core repositories. Development of TypeScript definitions for this version can be follow at feathersjs-ecosystem/feathers-typescript. Help welcome.

Backwards compatibility polyfills

Besides the steps outlined above, existing hooks, database adapters, services and other plugins should be fully compatible with Feathers v3 without any additional modifications.

This section contains some quick backwards compatibility polyfills for the breaking change that can be used to make the migration easier or continue to use plugins that use deprecated syntax.

Basic service filter

This is a basic emulation of the previous event filter functionality. It does not use promises or allow modifying the data (which should now be handled by setting hook.dispatch).

  1. app.mixins.push(service => {
  2. service.mixin({
  3. filter(eventName, callback) {
  4. const args = callback ? [ eventName ] : [];
  5. // todos.filter('created', function(data, connection, hook) {});
  6. if(!callback) {
  7. callback = eventName;
  8. }
  9. // A publisher function that sends to the `authenticated`
  10. // channel that we defined in the quick upgrade section above
  11. args.push((data, hook) => app.channel('authenticated')
  12. .filter(connection =>
  13. callback(data, connection, hook)
  14. )
  15. );
  16. service.publish(... args);
  17. }
  18. });
  19. });

Route parameters

  1. app.hooks({
  2. before(hook) {
  3. Object.assign(hook.params, hook.params.route);
  4. return hook;
  5. }
  6. })

.before and .after hook registration

  1. app.mixins.push(service => {
  2. service.mixin({
  3. before(before) {
  4. return this.hooks({ before });
  5. },
  6. after(after) {
  7. return this.hooks({ after });
  8. },
  9. })
  10. });