Serving angular client with node.js micro service

So you build your angularjs app and you need to serve the app in production. The common mistake is serving your client side from your main API. I believe the best option is just having a nodejs micro service that serving our static angular app.

The first problem serving angular app is the html5 routing system.

let's say this is your router:

$locationProvider.html5Mode(true);

$urlRouterProvider.when("/dashboard", "/dashboard/state1").otherwise("/login");

$stateProvider
  .state("app.login", {
    url: "/login",
    templateUrl: "templates/states/login/login.html",
  })
  .state("app.dashboard", {
    url: "/dashboard",
    templateUrl: "templates/states/dashboard/dashboard.html",
  });
$stateProvider.state("app.dashboard.state1", {
  url: "/state1",
  templateUrl: "templates/states/dashboard/state1/state1.html",
});
$stateProvider.state("app.dashboard.state2", {
  url: "/state2",
  templateUrl: "templates/states/dashboard/state2/state2.html",
});

Your urls will looks like that:

my-domain.com -> go to my-domain.com/login
my-domain.com/login
my-domain.com/dashboard -> go to my-domain.com/dashboard/state1
my-domain.com/dashboard/state1
my-domain.com/dashboard/state2

So if you normally go to my-domain.com it will work fine and angular will naturaly change the url to my-domain.com and will show the app.login state. Angular will do it well until you will try to reload or open my-domain.com/login or whatever route, your service will need to serve the index.html for all this routes.

First thing you will need to tell angular what is the base route. Add this line to your index.html

<head>
  ...
  <base href="/" />
  ...
</head>

Install dependencies

npm install express cors path compression --save

Create your server.js file

(function () {
  "use strict";

  // use new relic if license key exists
  if (typeof process.env.NEW_RELIC_LICENSE_KEY === "string") {
    require("newrelic");
  }

  var express = require("express"),
    cors = require("cors"),
    path = require("path"),
    compress = require("compression"),
    PORT = process.env.PORT || 3000,
    PRODUCTION_DOMAIN = "my-domain.com",
    APP_FOLDER = "build/release",
    RELEASE_PATH = path.resolve(__dirname, APP_FOLDER),
    INDEX_FILE = path.resolve(RELEASE_PATH, "index.html"),
    STATIC_URLS = [
      "/fonts/",
      "/icons/",
      "/images/",
      "/locales/",
      "/scripts/",
      "/styles/",
      "/favicons/",
    ],
    server = express();
  // Enable All CORS Requests
  server.use(cors());

  // use gzip compress
  server.use(compress());

  // // don't index unless production
  server.use(function (req, res, next) {
    if (req.hostname.indexOf(PRODUCTION_DOMAIN) < 0) {
      res.setHeader("X-Robots-Tag:", "noindex, nofollow");
    }

    return next();
  });

  // Cache the static urls
  server.use(function (req, res, next) {
    for (var i = 0; i < STATIC_URLS.length; i++) {
      if (req.url.indexOf(STATIC_URLS[i]) === 0) {
        res.setHeader("Cache-Control", "public, max-age=345600"); // 4 days
        res.setHeader(
          "Expires",
          new Date(Date.now() + 345600000).toUTCString(),
        );
      }
    }

    return next();
  });

  // use express to serve the static release files
  server.use(express.static(RELEASE_PATH));

  // support html5mode for angular
  // serve the index file
  server.get("/*", function (req, res, next) {
    // Do not serve the index for static urls
    for (var i = 0; i < STATIC_URLS.length; i++) {
      if (req.url.indexOf(STATIC_URLS[i]) === 0) {
        return next();
      }
    }

    // Use res.sendFile, as it streams instead of reading the file into memory.
    res.sendFile(INDEX_FILE);
  });

  // run the server and start listen to the port
  server.listen(PORT);

  // Render some console log output
  console.log("Angular app is running on port " + PORT);
})();

Just run

node server.js

You can run it on your favorite platform.