Sherman
Sherman

Reputation: 897

Proxy Routing on Angular App deployed on Heroku

I have an Angular app that is talking to a REST service.

When I run the Angular app local with the CLI, correctly proxies all /api requests to the REST service. When I try to build the app and run through a server.js (so that I can deploy the app to Heroku) I lose the proxy routing.

The REST service is deployed on Heroku and runs fine.

I run the Angular with:

ng serve

My proxy.conf.json

{
  "/api": {
    "target": "https://my-app.herokuapp.com",
    "secure": true,
    "changeOrigin": true
  }
}

I created a server.js as described in this article so that I can deploy onto Heroku.

// server.js
const express = require('express');
const app = express();
const path = require('path');

// If an incoming request uses
// a protocol other than HTTPS,
// redirect that request to the
// same url but with HTTPS
const forceSSL = function () {
    return function (req, res, next) {
        if (req.headers['x-forwarded-proto'] !== 'https') {
            return res.redirect(
                ['https://', req.get('Host'), req.url].join('')
            );
        }
        next();
    }
}
// Instruct the app
// to use the forceSSL
// middleware
app.use(forceSSL());

// Run the app by serving the static files
// in the dist directory
app.use(express.static(__dirname + '/dist'));

// For all GET requests, send back index.html
// so that PathLocationStrategy can be used

app.get('/*', function (req, res) {
    res.sendFile(path.join(__dirname + '/dist/index.html'));
});

// Start the app by listening on the default
// Heroku port
app.listen(process.env.PORT || 4200);

I also set up a post install build in my package.json scripts:

{
  "name": "catalog-manager-client",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "node server.js",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "postinstall": "ng build --aot"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^6.0.3",
    "@angular/cdk": "^6.2.1",
    "@angular/cli": "~6.0.8",
    "@angular/language-service": "^6.0.3",
    "@angular/common": "^6.0.3",
    "@angular/compiler": "^6.0.3",
    "@angular/compiler-cli": "^6.0.3",
    "@angular/core": "^6.0.3",
    "@angular/flex-layout": "^6.0.0-beta.16",
    "@angular/forms": "^6.0.5",
    "@angular/http": "^6.0.3",
    "@angular/material": "^6.2.1",
    "@angular/platform-browser": "^6.0.3",
    "@angular/platform-browser-dynamic": "^6.0.3",
    "@angular/router": "^6.0.3",
    "@swimlane/ngx-charts": "^8.0.2",
    "@swimlane/ngx-datatable": "^13.0.1",
    "core-js": "^2.5.4",
    "express": "^4.16.4",
    "hammerjs": "^2.0.8",
    "jquery": "^3.3.1",
    "moment": "^2.22.2",
    "ngx-perfect-scrollbar": "^6.2.0",
    "ngx-quill": "^3.2.0",
    "rxjs": "^6.0.0",
    "rxjs-compat": "^6.2.1",
    "rxjs-tslint": "^0.1.4",
    "zone.js": "^0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.6.8",
    "typescript": "~2.7.2",
    "@types/jasmine": "~2.8.6",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~8.9.4",
    "codelyzer": "~4.2.1",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~1.7.1",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.0",
    "karma-jasmine": "~1.1.1",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.3.0",
    "ts-node": "~5.0.1",
    "tslint": "~5.9.1"
  },
  "engines": {
    "node": "9.11.2",
    "npm": "6.5.0"
  }
}

I am an Angular novice so I could be making a fundamental mistake, but how do I modify the server.js to use the proxy.conf.json settings?

Upvotes: 3

Views: 2393

Answers (2)

Prem Kumar
Prem Kumar

Reputation: 1

Any One looking for Implementation of angular application using proxy api on heroku you can use WebpackDev Server and http-proxy-middleware in server.js

npm install http-proxy-middleware

npm install webpack webpack-dev-server

webpack.config.js

const path = require('path');
module.exports = {
    entry:'./src/index.js',//no implemenation needed by default webpack verification
    mode: 'development',   
    devServer: {
    historyApiFallback: true,// handle 404 cannot get error after refreshing url
    https: true,//secure the server
    compress: true,//invalid header multiple url proxy
    client: {
        webSocketURL: 'ws://0.0.0.0:8080/ws',// handle Invalid header error  in heroku port 8080 maps in server.js
      },
      static: {
        directory: path.join(__dirname, '/dist/<app-name>'),
      },

      proxy: {
    /** Same as proxy.conf.json or proxy.conf.js */
        ' /api1/*': {
          target: 'https://<other-heroku-deployed-url>',
          changeOrigin:true,
          secure:false,
          pathRewrite: {
          '^/api1':'https://<other-heroku-deployed-url>/api1'                           },
        }, 
        ' /api2/*': {
          target: 'https://<other-heroku-deployed-url>',
          changeOrigin:true,
          secure:false,
          pathRewrite: {
          '^/api2':'https://<other-heroku-deployed-url>/api2'                           },
        }
      },
    },
   
  };

server.js

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
/** this is custom js to help proxy in server.js*/
const webpackConfig = require('./webpack.config.js');


const compiler = Webpack(webpackConfig);
const devServerOptions = { ...webpackConfig.devServer, open: true };
const server = new WebpackDevServer(devServerOptions, compiler);

const runServer = async () => {
  console.log('Starting server...');
  await server.start();
};

runServer();
/** If you have error creating proxy <app-url> to localhost  
 * Heroku internally redirect the Server port 8080 .
 *  For that reason we need to open listener  port(I used 3000 here) redirect 
through http-proxy-middleware*/
 app.use("/*", createProxyMiddleware(
    { target: "https://localhost:8080", 
    ws: true ,
     changeOrigin: true,
     secure:false,
     router: {
    'dev.localhost:3000': 'https://localhost:8080',
  },})) 

app.listen(process.env.PORT || 3000)

npm start or node server.js

Upvotes: 0

Sherman
Sherman

Reputation: 897

The explanation falls into the yes, you're making a fundamental mistake category, but I've seen enough similar questions that I thought an explanation might just help the next dev.

The Angular CLI is running a full http server. The Angular UI is fully compiled and the CLI is serving it as static content from the /dist directory.

The proxy.conf.json settings are for the Server run by the Angular CLI, it has nothing to do with your compiled code.

When you move from a local environment to something like Heroku you need a server to take the place of the Angular CLI. This is where all the examples of node.js and express come in. The simple server.js file they walk you through is enough to set up a basic static content server. And that's fine, because your Angular code is static content!

But if you need routing to a dynamic backend server via a proxy.conf.json, well, your simple static server doesn't know anything about that.

In my case, my backend server runs on Koa, so I added static routing to the Angular code.

const router = require('koa-router')();
const body = require('koa-body')({ text: false });
const send = require('koa-send');
const fs = require('fs');

/**
 * Code about server routes ommited
 */

async function main(ctx, next) {
    //All dynamic routes start with "/api"
    if (/\/api\//.test(ctx.path)) {
        try {
            await next();
        }
        catch (error) {
            if (error instanceof ApplicationError) {
                logger.error(error, { data: error.data, stack: error.stack });
                ctx.status = error.code;
            } else {
                ctx.status = 500;
                logger.error(error.message, { stack: error.stack });
            }
        }
        return;
    } else {
        //Not a dynamic route, serve static content
        if ((ctx.path != "/") && (fs.existsSync('dist' + ctx.path))) {
            await send(ctx, 'dist' + ctx.path);
        } else {
            await send(ctx, 'dist/index.html');
        }
    }
}

module.exports = app => {
    app.use(main);
    app.use(router.routes());
};

NOTE - this isn't a performant solution for any kind of high workload, but if you've got a very small project that doesn't justify spending resources setting up something more scalable, this will work until you get bigger.

Upvotes: 2

Related Questions