HTTP/2 with Feathers and Express


#1

I’ve collected some links about how the native HTTP/2 module doesn’t work with Express.

Since I’m using Feathers, I’ll be sticking to using @feathersjs/express with spdy until express@5.0 is used and supports the native http2 module, as it seems fastify in feathers might not be straightforward.

Here is what my server code looks like (seems to work as expected):

config/development.json

{
  "port": 3030,
  "host": "DEV_SERVER_HOST",
  "client": "../../client/",
  "mongodb": "mongodb://localhost:27017/database",
  "key": "localhost+2-key.pem",
  "cert": "localhost+2.pem"
}

Note: key/cert generated using https://github.com/FiloSottile/mkcert.

index.js

'use strict';

// ENVIRONMENT

if (process.argv.indexOf('--develop') !== -1) {
  process.env.DONE_SSR_DEBUG = 1;
  process.env.NODE_ENV = 'development';
}

console.log(`process.env.NODE_ENV = '${process.env.NODE_ENV}'`);

// app server

const fs = require('fs');
const path = require('path');
const spdy = require('spdy');
const app = require('./app');

const host = app.get('host');
const port = app.get('port');
const key = path.resolve(__dirname, app.get('key'));
const cert = path.resolve(__dirname, app.get('cert'));

const server = spdy
  .createServer(
    {
      key: fs.readFileSync(key),
      cert: fs.readFileSync(cert),
    },
    app
  )
  .listen(port);

app.setup(server);

server.on('listening', () =>
  console.log(
    `DoneJS and Feathers application started on https://${host}:${port}`
  )
);

// live-reload

if (process.argv.indexOf('--develop') !== -1) {
  const exec = require('child_process').exec;
  const killOnExit = require('kill-on-exit');

  const stealTools = path.join('node_modules', '.bin', 'steal-tools');

  const child = exec(
    `${stealTools} live-reload --ssl-key ${key} --ssl-cert ${cert}`,
    {
      cwd: app.get('client'),
    }
  );

  child.stdout.pipe(process.stdout);
  child.stderr.pipe(process.stderr);

  killOnExit(child);
}

app.js

'use strict';

const path = require('path');
const cors = require('cors');
const services = require('./services');
const favicon = require('serve-favicon');
const compression = require('compression');
const middleware = require('./middleware');
const express = require('@feathersjs/express');
const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio');
const configuration = require('@feathersjs/configuration');

const app = express(feathers());

app
  .configure(configuration())
  .options('*', cors())
  .use(cors())
  .use(compression())
  .use(favicon(path.join(app.get('client'), 'favicon.ico')))
  .use(express.json())
  .use(express.urlencoded({ extended: true }))
  .configure(express.rest())
  .configure(socketio())
  .configure(services)
  .use(express.static(app.get('client')))
  .use('/node_modules', (req, res) => {
    res.sendStatus(404);
  })
  .configure(middleware);

app.on('connection', connection => {
  app.channel('anonymous').join(connection);
});

app.publish((data, context) => {
  return app.channel('anonymous');
});

module.exports = app;

middleware/index.js

'use strict';

const errorHandler = require('./error');

module.exports = function(app) {
  if (process.argv.indexOf('--static') !== -1) {
    console.log('Configuring server app with static middleware.');
    app.configure(require('./static'));
  } else {
    console.log('Configuring server app with SSR middleware.');
    app.configure(require('./ssr'));
  }

  app.configure(errorHandler);
};

middleware/static.js

'use strict';

const fs = require('fs');
const path = require('path');

module.exports = function(app) {
  app.get('*', (req, res, next) => {
    const urlParts = path.parse(req.url);
    const isPushstateRoute = !urlParts.ext || urlParts.name.includes('?');

    if (isPushstateRoute) {
      const env = process.env.NODE_ENV || 'production';
      const htmlPath = path.join(app.get('client'), `${env}.html`);

      if (fs.existsSync(htmlPath)) {
        return res.sendFile(htmlPath);
      }
    }

    return next();
  });
};

middleware/ssr.js

'use strict';

const path = require('path');
const ssrMiddleware = require('done-ssr-middleware');

module.exports = function(app) {
  app.use(
    ssrMiddleware(
      {
        config: path.join(app.get('client'), 'package.json!npm'),
        liveReload: process.env.NODE_ENV === 'development',
      },
      {
        strategy: 'incremental',
        debug: process.env.NODE_ENV === 'development',
      }
    )
  );
};

Note: using strategy: 'incremental' for incremental rendering works.

middleware/error.js

'use strict';

const errorFormat = require('donejs-error-format');

module.exports = function(app) {
  app.use((err, req, res, next) => {
    if (res.headersSent) {
      return next(err);
    }

    const html = errorFormat.html(errorFormat.extract(err), {
      liveReload: process.env.NODE_ENV === 'development',
    });

    console.error(err.stack);

    res
      .status(500)
      .type('html')
      .end(html);
  });
};

services/index.js

'use strict';

const mongoose = require('mongoose');

// require services
const contributor = require('./contributor');

module.exports = function(app) {
  mongoose.Promise = global.Promise;

  // connect to mongodb
  if (process.env.TESTING !== 'true') {
    mongoose.connect(
      app.get('mongodb'),
      {
        useCreateIndex: true,
        useNewUrlParser: true,
        reconnectTries: Number.MAX_VALUE,
      }
    );
  }

  // configure endpoints
  app.configure(contributor);
};

services/contributor/index.js

'use strict';

const Model = require('./model');
const service = require('feathers-mongoose');

module.exports = function(app) {
  app.use(
    '/api/contributors',
    service({
      Model: Model,
      lean: true,
    })
  );

  app.service('/api/contributors').hooks({
    after(context) {
      context.data && delete context.data.__v;
    },
  });
};

services/contributor/model.js

'use strict';

const mongoose = require('mongoose');

const Schema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  active: { type: Boolean },
});

const Model = mongoose.model('Contributor', Schema);

module.exports = Model;

Result

Hope this helps someone else trying to get h2 working with feathers express.

More links about server push (for future self reference):


#2

Wow. Thanks for sharing!