Skip to content

Commit

Permalink
Split Express app setup from server setup
Browse files Browse the repository at this point in the history
Separates their concerns: middleware, routing, handlers, etc. belong to
the app while CLI argument parsing, ports, listening, etc. belong to the
server.
  • Loading branch information
tsibley committed Oct 27, 2021
1 parent 49ba156 commit 8064627
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 148 deletions.
178 changes: 30 additions & 148 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
/* eslint no-console: off */
const path = require("path");
const sslRedirect = require('heroku-ssl-redirect');
const nakedRedirect = require('express-naked-redirect');
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
const favicon = require('serve-favicon');
const compression = require('compression');
const argparse = require('argparse');
const utils = require("./src/utils");
const { potentialAuspiceRoutes, isRequestBackedByAuspiceDataset } = require('./src/auspicePaths');
const cors = require('cors');
const {addAsync} = require("@awaitjs/express");
const {NotFound} = require('http-errors');
const http = require("http");

const production = process.env.NODE_ENV === "production";
const utils = require("./src/utils");

const version = utils.getGitHash();
const nextstrainAbout = `
Expand All @@ -33,139 +21,33 @@ parser.addArgument('--verbose', {action: "storeTrue", help: "verbose server logg
const args = parser.parseArgs();
global.verbose = args.verbose;


// Import these after parsing CLI arguments and setting global.verbose so code
// in them can use utils.verbose() at load time.
const auspiceServerHandlers = require("./src/index.js");
const authn = require("./src/authn");
const redirects = require("./src/redirects");

/* Path helpers for static assets, to make routes more readable.
*/
const relativePath = (...subpath) =>
path.join(__dirname, ...subpath);

const gatsbyAssetPath = (...subpath) =>
relativePath("static-site", "public", ...subpath);

const auspiceAssetPath = (...subpath) =>
relativePath("auspice-client", ...subpath);


/* BASIC APP SETUP */
// NOTE: order of app.get is first come first serve (https://stackoverflow.com/questions/32603818/order-of-router-precedence-in-express-js)
const app = addAsync(express());

// In production, trust Heroku as a reverse proxy and Express will use request
// metadata from the proxy.
if (production) app.enable("trust proxy");

app.set('port', process.env.PORT || 5000);
app.use(sslRedirect()); // redirect HTTP to HTTPS
app.use(compression()); // send files (e.g. res.json()) using compression (if possible)
app.use(nakedRedirect({reverse: true})); // redirect www.nextstrain.org to nextstrain.org
app.use(favicon(relativePath("favicon.ico")));
app.use('/favicon.png', express.static(relativePath("favicon.png")));


/* Authentication (authn)
*/
authn.setup(app);


/* Redirects.
*/
redirects.setup(app);


/* Static assets.
* Any paths matching resources here will be handled and not fall through to our handlers below.
* E.g. URL `/influenza` gets sent `static-site/public/influenza/index.html`
*/
app.use(express.static(gatsbyAssetPath()));
app.route("/dist/*") // Auspice hardcodes /dist/… in its Webpack config.
.all(expressStaticGzip(auspiceAssetPath(), {maxAge: '30d'}));


/* Charon API used by Auspice.
*/
app.routeAsync("/charon/getAvailable")
.all(cors({origin: 'http://localhost:8000'})) // allow cross-origin from the gatsby dev server
.getAsync(auspiceServerHandlers.getAvailable);

app.routeAsync("/charon/getDataset")
.getAsync(auspiceServerHandlers.getDataset);

app.routeAsync("/charon/getNarrative")
.getAsync(auspiceServerHandlers.getNarrative);

app.routeAsync("/charon/getSourceInfo")
.getAsync(auspiceServerHandlers.getSourceInfo);

app.routeAsync("/charon/*")
.all((req) => {
utils.warn(`(${req.method}) ${req.url} has not been handled / has no handler`);
throw new NotFound();
});

/* Specific routes to be handled by Gatsby client-side routing
*/
app.route([
"/users/:user",
"/groups",
"/groups/:groupName"
]).get((req, res) => res.sendFile(gatsbyAssetPath("index.html")));

/* Routes which are plausibly for auspice, as they have specific path signatures.
* We verify if these are are backed by datasets and hand to auspice if so.
*/
app.route(potentialAuspiceRoutes).get(
isRequestBackedByAuspiceDataset,
sendAuspiceHandler,
sendGatsbyHandler
);

/**
* Finally, we catch all remaining routes and send to Gatsby
*/
app.get("*", sendGatsbyHandler);


const server = app.listen(app.get('port'), () => {
console.log(" -------------------------------------------------------------------------");
console.log(nextstrainAbout);
console.log(` Server listening on port ${server.address().port}`);
console.log(` Accessible at https://nextstrain.org or http://localhost:${server.address().port}`);
console.log(` Server is running in ${production ? 'production' : 'development'} mode`);
console.log("\n -------------------------------------------------------------------------\n\n");
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {
utils.error(`Port ${app.get('port')} is currently in use by another program.
You must either close that program or specify a different port by setting the shell variable
"$PORT". Note that on MacOS / Linux, "lsof -n -i :${app.get('port')} | grep LISTEN" should
identify the process currently using the port.`);
}
utils.error(`Uncaught error in app.listen(). Code: ${err.code}`);
});


function sendGatsbyHandler(req, res) {
utils.verbose(`Sending Gatsby entrypoint for ${req.originalUrl}`);
return res.sendFile(
gatsbyAssetPath(""),
{},
(err) => {
// callback runs when the transfer is complete or when an error occurs
if (err) res.status(404).sendFile(gatsbyAssetPath("404.html"));
const port = process.env.PORT || 5000;

/* Import app after setting global.verbose so that calls to utils.verbose()
* respect our --verbose option as expected.
*/
const app = require("./src/app");

app.set("port", port);

const server = http.createServer(app)
.on("listening", () => {
console.log(" -------------------------------------------------------------------------");
console.log(nextstrainAbout);
console.log(` Server listening on port ${server.address().port}`);
console.log(` Accessible at https://nextstrain.org or http://localhost:${server.address().port}`);
console.log(` Server is running in ${app.locals.production ? 'production' : 'development'} mode`);
console.log("\n -------------------------------------------------------------------------\n\n");
})
.on("error", (err) => {
if (err.code === 'EADDRINUSE') {
utils.error(`Port ${app.get('port')} is currently in use by another program.
You must either close that program or specify a different port by setting the shell variable
"$PORT". Note that on MacOS / Linux, "lsof -n -i :${app.get('port')} | grep LISTEN" should
identify the process currently using the port.`);
}
);
}
utils.error(`Uncaught error in app.listen(). Code: ${err.code}`);
})
;

function sendAuspiceHandler(req, res, next) {
if (!req.sendToAuspice) return next(); // see previous middleware
utils.verbose(`Sending Auspice entrypoint for ${req.originalUrl}`);
return res.sendFile(
auspiceAssetPath("dist", "index.html"),
{headers: {"Cache-Control": "no-cache, no-store, must-revalidate"}}
);
}
server.listen(port);
134 changes: 134 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint no-console: off */
const path = require("path");
const sslRedirect = require('heroku-ssl-redirect');
const nakedRedirect = require('express-naked-redirect');
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
const favicon = require('serve-favicon');
const compression = require('compression');
const utils = require("./utils");
const { potentialAuspiceRoutes, isRequestBackedByAuspiceDataset } = require('./auspicePaths');
const cors = require('cors');
const {addAsync} = require("@awaitjs/express");
const {NotFound} = require('http-errors');

const production = process.env.NODE_ENV === "production";

const auspiceServerHandlers = require("./index.js");
const authn = require("./authn");
const redirects = require("./redirects");

/* Path helpers for static assets, to make routes more readable.
*/
const relativePath = (...subpath) =>
path.join(__dirname, "..", ...subpath);

const gatsbyAssetPath = (...subpath) =>
relativePath("static-site", "public", ...subpath);

const auspiceAssetPath = (...subpath) =>
relativePath("auspice-client", ...subpath);


/* BASIC APP SETUP */
// NOTE: order of app.get is first come first serve (https://stackoverflow.com/questions/32603818/order-of-router-precedence-in-express-js)
const app = addAsync(express());

app.locals.production = production;

// In production, trust Heroku as a reverse proxy and Express will use request
// metadata from the proxy.
if (production) app.enable("trust proxy");

app.use(sslRedirect()); // redirect HTTP to HTTPS
app.use(compression()); // send files (e.g. res.json()) using compression (if possible)
app.use(nakedRedirect({reverse: true})); // redirect www.nextstrain.org to nextstrain.org
app.use(favicon(relativePath("favicon.ico")));
app.use('/favicon.png', express.static(relativePath("favicon.png")));


/* Authentication (authn)
*/
authn.setup(app);


/* Redirects.
*/
redirects.setup(app);


/* Static assets.
* Any paths matching resources here will be handled and not fall through to our handlers below.
* E.g. URL `/influenza` gets sent `static-site/public/influenza/index.html`
*/
app.use(express.static(gatsbyAssetPath()));
app.route("/dist/*") // Auspice hardcodes /dist/… in its Webpack config.
.all(expressStaticGzip(auspiceAssetPath(), {maxAge: '30d'}));


/* Charon API used by Auspice.
*/
app.routeAsync("/charon/getAvailable")
.all(cors({origin: 'http://localhost:8000'})) // allow cross-origin from the gatsby dev server
.getAsync(auspiceServerHandlers.getAvailable);

app.routeAsync("/charon/getDataset")
.getAsync(auspiceServerHandlers.getDataset);

app.routeAsync("/charon/getNarrative")
.getAsync(auspiceServerHandlers.getNarrative);

app.routeAsync("/charon/getSourceInfo")
.getAsync(auspiceServerHandlers.getSourceInfo);

app.routeAsync("/charon/*")
.all((req) => {
utils.warn(`(${req.method}) ${req.url} has not been handled / has no handler`);
throw new NotFound();
});

/* Specific routes to be handled by Gatsby client-side routing
*/
app.route([
"/users/:user",
"/groups",
"/groups/:groupName"
]).get((req, res) => res.sendFile(gatsbyAssetPath("index.html")));

/* Routes which are plausibly for auspice, as they have specific path signatures.
* We verify if these are are backed by datasets and hand to auspice if so.
*/
app.route(potentialAuspiceRoutes).get(
isRequestBackedByAuspiceDataset,
sendAuspiceHandler,
sendGatsbyHandler
);

/**
* Finally, we catch all remaining routes and send to Gatsby
*/
app.get("*", sendGatsbyHandler);


function sendGatsbyHandler(req, res) {
utils.verbose(`Sending Gatsby entrypoint for ${req.originalUrl}`);
return res.sendFile(
gatsbyAssetPath(""),
{},
(err) => {
// callback runs when the transfer is complete or when an error occurs
if (err) res.status(404).sendFile(gatsbyAssetPath("404.html"));
}
);
}

function sendAuspiceHandler(req, res, next) {
if (!req.sendToAuspice) return next(); // see previous middleware
utils.verbose(`Sending Auspice entrypoint for ${req.originalUrl}`);
return res.sendFile(
auspiceAssetPath("dist", "index.html"),
{headers: {"Cache-Control": "no-cache, no-store, must-revalidate"}}
);
}

module.exports = app;

0 comments on commit 8064627

Please sign in to comment.