koajs / examples

Example Koa apps
4.52k stars 744 forks source link

Virtual Host Example #10

Closed rynz closed 10 years ago

rynz commented 10 years ago

Please create an example similar to https://github.com/visionmedia/express/tree/master/examples/vhost as it would be invaluable :)

jonathanong commented 10 years ago

right now, you can do the same, except with app.callback() with koa apps. probably won't add it to the examples until we make vhost its own repo, which we'll do in connect 3 / express 4

rynz commented 10 years ago

@jonathanong Could you please reference an app.callback() vhost example with koa apps in this ticket in the meantime please?

jonathanong commented 10 years ago

well it's the same except instead of app.use(express.vhost('example.com', main)); it'll be app.use(express.vhost('example.com', main.callback()));. this is assuming app is a connect/express app, and main is a koa app.

rynz commented 10 years ago

I think I've misinterpreted koajs. Can I not write a web app purely in koajs without express/connect?

jonathanong commented 10 years ago

yes. that's why it's not an example. vhost is a connect/express middleware, and there isn't one for koa, yet.

rynz commented 10 years ago

Okay thanks for your help @jonathanong - Do you have a timeline for the koajs vhost middleware? It seems to be the only thing really missing currently and koajs is so awesome I can't wait to use it! Maybe I could have a crack at porting the express version to koajs to be merged with the main repo?

jonathanong commented 10 years ago

well.. connect's pretty much works without connect. it's just a shame it's a part of connect, which is why we're moving it to its own repo in connect 3. we're not going to add it to koa though, it should still be its own module/middleware.

you could make your own if you want. i think it should sit between the server/apps. the api should look like this:

var vhost = require('vhost')()
var server = http.createServer(vhost)

vhost.use('*.example.com', connect())
vhost.use('example.com', koa().callback())
server.listen()
rynz commented 10 years ago

Ah so no koa-vhost (https://github.com/koajs/vhost) in the future? That's a shame. Okay I'll roll my own for now and replace it with connect's when it is separated out. Thanks for your support.

jonathanong commented 10 years ago

there's no need. there's no need for koa- this and express- that when they work just fine on their own.

rynz commented 10 years ago

Yeah I agree but I kind of like the idea of koa- for "officially supported" generator based middleware I guess. If you could please update this ticket / documentation in koajs somewhere when vhost is it's own module/middleware that would be greatly appreciated.

jonathanong commented 10 years ago

here you go! https://github.com/expressjs/vhostess if you want an example up quickly, open a PR :)

jonathanong commented 10 years ago

unless you want to do mounting... which I'm -1 (for the most part).

rynz commented 10 years ago

@jonathanong Fantastic thank you. I guess this is where a koa-vhost relevance / question might come in. I'm curious if generators could be used with vhostess for more elegant global middleware?

Eg: Express you can do the following:

var app = express();
var expressApp = express();
var secondExpressApp = express();

// Log ALL requests.
app.use(function(req, res, next) {
  req.startTime = new Date;

  function logRequest() {
    res.removeListener('finish', logRequest);
    res.removeListener('close', logRequest);
    var ms = new Date - req.startTime;
    console.log('%s %s - %s', req.method, req.originalUrl || req.url, ms);
  };

  res.on('finish', logRequest);
  res.on('close', logRequest);

  next();
});

app.use(express.vhost('express.app.com', expressApp));
app.use(express.vhost('secondexpress.app.com', secondExpressApp));

Which would provide a global middleware for logging all vhost application requests. If vhostess or an alternative koa-vhost implementation were supported, we could write elegant generator based global middleware rather than implementing the same middleware in each koa app separately.

Eg: Koa example derived from above express example

var hostess = require('vhostess')();
var koaApp = koa();
var secondKoaApp = koa();

// Log ALL requests.
hostess.use(function *(next) {
  var start = new Date();
  yield next;
  var ms = new Date() - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

hostess.use('express.app.com', koaApp.callback());
hostess.use('secondexpress.app.com', secondKoaApp.callback());

Which achieves the same global middleware with generators. The global middleware isn't only specific to logging and has many use cases, eg: Global Cross Origin Request middleware for APIs etc.

jonathanong commented 10 years ago

not sure why you need to do that. just app.use(logger) in each app. you can also bundle a bunch of middleware together using koa-compose to make your life easier.

koa also has this.subdomains, so i'm not sure how useful a vhost middleware would be. connect on the other hand does not have req.subdomains.

maybe i just need an example where vhost as a middleware is required.

rynz commented 10 years ago

I guess koa-compose is a good solution too in certain situations. I was just thinking that for example a single node instance may run multiple APIs for unique products that all depend on some common code / authentication session etc. Eg: Logging, CORS, Authentication, Validation, Error Handling etc.

Being able to mix in global middleware at the vhost level means we can yield at different points of the request/response lifecycle.

Eg:

// koa-logger
app.use(logger());
// koa-cors
app.use(cors());
// koa-etag
app.use(etag());
// koa-session
app.use(session());
// koa-compress
app.use(compress());
// generator version of express-validator
app.use(validator());

// Authentication Api.
app.use(koa.vhost('auth.mycompany.com', authApp));

// Generator for authentication testing shared across all App Apis.
// Eg: Session, token, facebook, twitter etc, one authorisation test for all Apps
// when successfully achieved at `auth.mycompany.com`.
// Responds with generic 401 if the user is not authenticated.
app.use(authentication());

// Other middleware which records metrics of logged in users etc..

// App Apis.
app.use(koa.vhost('awesomeapp.mycompany.com', awesomeApp));
app.use(koa.vhost('fantasticapp.mycompany.com', fantasticApp));
app.use(koa.vhost('realtimeapp.mycompany.com', realtimeApp));

// Common JSON Response errors from Apis.
app.use(function *(next) {
  yield next;
  // 404
  if (null == body && 200 == this.status) {
    this.status = 404;
    this.body = {code: 404, message: 'Ah, where am I?'};
  }
  // 500
  if (500 == this.status) {
    this.body = {code: 500, message: 'Kaboom!'};
  }
});

// Error logging.
app.on('error', function(err) {
  log.error('server error', err);
});

app.on('error', function(err, ctx) {
  log.error('server error', err, ctx);
});

app.listen(3000);

Which is where I think it would be beneficial for a koa-vhost specific module that behaves like a koa application to achieve this similar to express does.

If I was to use koa-compose it would work for the initial generators, however being able to use a generator mid way through and at the end would not be possible. We would have to be very careful to have the same dependencies at the same points in each app and if they were to be defined globally this way, you can clearly understand common functionality between each.

Thoughts?

jonathanong commented 10 years ago

in that case, i would just do:

app.use(function*(next){
  if (this.host === 'api.awesomeapp.com') yield awesomeApp.call(this, next);
  else yield next;
})

since you're doing multiple subdomains, i would do one large switch statement.

although this could be its own repo, i'd personally rather teach people how to do this instead.

of course you'll run into different issues. you'll have to use compose(awesomeApp.middleware) and use that generator instead. in fact, i wouldn't even bother creating separate apps - just create different bundles of middleware.

note that subapps in koa are not encapsulated or inherited like in express. all the subapps will use the main app's context and keys, and there's no way it can't. to solve this, you'll have to use vhostess and do separate .callback()s

If I was to use koa-compose it would work for the initial generators, however being able to use a generator mid way through and at the end would not be possible. We would have to be very careful to have the same dependencies at the same points in each app and if they were to be defined globally this way, you can clearly understand common functionality between each.

i'm not sure what this means

rynz commented 10 years ago

By Generators / Dependencies in that last phrase I meant "koa based" middleware sorry.

Sorry I'm a little confused, could you please show me an example with "global" middleware at the start, between two hosts and at the end?

jonathanong commented 10 years ago

I'm not sure what you mean by "global" or "at the end". For each vhost in your example, I would use a conditional middleware as in my comment above.

Can you give a different example?

rynz commented 10 years ago

Sure, how would you re-write this using your proposed solution taking into account compose() issue and not creating separate apps with an example of the same GET /test request outputting different JSON body for each host.

Eg:

// koa-logger
app.use(logger());
// koa-cors
app.use(cors());

// Authentication Api, with with GET /test Route {from: 'auth'}
app.use(koa.vhost('auth.mycompany.com', authApp));

// Placeholder for Authentication Middleware.
// Will only be used if `auth.mycompany.com` was not processed.
app.use(function *(next) {
  console.log('Test App Authenticated');
  yield next;
});

// App Apis.
// with GET /test Route {from: 'awesomeapp'}
app.use(koa.vhost('awesomeapp.mycompany.com', awesomeApp));
// with GET /test Route {from: 'fantasticapp'}
app.use(koa.vhost('fantasticapp.mycompany.com', fantasticApp));

// Common JSON Response errors from Apps.
app.use(function *(next) {
  yield next;
  // 404
  if (null == body && 200 == this.status) {
    this.status = 404;
    this.body = {code: 404, message: 'Ah, where am I?'};
  }
  // 500
  if (500 == this.status) {
    this.body = {code: 500, message: 'Kaboom!'};
  }
});

// Error logging.
app.on('error', function(err) {
  log.error('server error', err);
});

app.on('error', function(err, ctx) {
  log.error('server error', err, ctx);
});

app.listen(3000);
jonathanong commented 10 years ago
// assuming all apps are koa apps, "compose" them first.
var compose = require('koa-compose')
authApp = compose(authApp.middleware)
awesomeApp = compose(awesomeApp.middleware)
fantasticApp = compose(fantasticApp.middleware)

// koa-logger
app.use(logger());
// koa-cors
app.use(cors());

app.use(function* (next) {
  // Authentication Api, with with GET /test Route {from: 'auth'}
  if (this.host === 'auth.mycompany.com') yield authApp.call(this, next);
  // Placeholder for Authentication Middleware.
  else yield otherAuthentication.call(this, next);
})

// App Apis.
app.use(function* (next) {
  switch (this.host) {
    // with GET /test Route {from: 'awesomeapp'}
    case 'awesomeapp.mycompany.com': return yield awesomeApp.call(this, next);
    // with GET /test Route {from: 'fantasticapp'}
    case 'fantasticapp.mycompany.com': return yield fantasticApp.call(this, next);
  }
  // everything else
  yield next;
})

// Common JSON Response errors from Apps.
app.use(function *(next) {
  yield next;
  // 404
  if (null == body && 200 == this.status) {
    this.status = 404;
    this.body = {code: 404, message: 'Ah, where am I?'};
  }
  // 500
  if (500 == this.status) {
    this.body = {code: 500, message: 'Kaboom!'};
  }
});

// Error logging.
app.on('error', function(err) {
  log.error('server error', err);
});

app.on('error', function(err, ctx) {
  log.error('server error', err, ctx);
});

app.listen(3000);

your error handler is actually incorrect, but that's an entirely different issue. don't think we have good error handling docs or examples yet

rynz commented 10 years ago

Great thank you, yes I fixed the error handling with try yield / catch error to catch upstream errors correctly. You mentioned using different bundles of middleware rather than separate koa apps. How would you go about achieving this?

jonathanong commented 10 years ago

it's fine like this as long as you compose them. probably would be better to keep them like this (using apps) if you ever plan to use them separately. also, doing only app.use() makes life simpler vs. mixing apps and arrays.

otherwise, i would just do var stack = [], stack.push(function*(){}) your middleware, then app.use(compose(stack)).

rynz commented 10 years ago

Okay fantastic thank you for all your help. Would you like me to PR an example for others to learn from?

jonathanong commented 10 years ago

sure. i was going to do it eventually, but i think your case is a good test case.

jonathanong commented 10 years ago

it's basically a special type of conditional middleware. the only thing that isn't supported is something like *.example.com, but in that case, doing this.subdomains.length === 1 is probably better.

rynz commented 10 years ago

PR https://github.com/koajs/examples/pull/18