Continuous Development - Building an Angular application
Do you dream a local development environment easy to configure, that works independently from the software layers that you are currently not working on? Me too!

Do you dream a local development environment easy to configure, that works independently from the software layers that you are currently not working on? Me too!

As software engineer, I have always suffered the pain of starting to work on projects that were not so easy to configure. Tons of minutes spent on technical documentation, often not updated or with many missing steps, to understand why my local development environment was not working.

#The Ideal Scenario

Continuous Development - Building an Angular application

My dream would be to have a single and short (as possible) document that explains how to configure, build, publish, check code quality and run automation tests on the project I will work on for the next months. For sure, there is a list of prerequisites that a developer needs to respect before starting to contribute to a project: know how to use the IDE, the Version of Control, how to use a package manager, etc. But nothing more: you don’t need to know a made-in-house (with poor documentation) framework just to satisfy the ego of an architect that wanted to reinvent the wheel. You don’t need to run external virtual machine to emulate the production environment. You don’t need to maintain your virtual machine, in order to be similar to the production environment. You need to invest your time on improving your code and on adding value to your product.

#Use-case: dev expectations while working to an Angular application

The aim of this section is to describe the strategy followed to build an Angular 8 application, where the developer experience was at the center. The fact that it is a client application, it does not mean that similar techniques cannot be applied to back-end modules. The fact that the framework that has been used is Angular, it does not mean that similar techniques cannot be applied to Vanilla or whatever framework you prefer (e.g. React, Vue). We will not go too much in details with the domain and the business logic: it is a simple web app, with authentication, which performs several calls to REST endpoints. Given the assumptions above, let’s look which requirements we are going to satisfy and how. Disclaimer: few details related to used libraries, and Angular itself, will be necessary to understand the strategies, but I am pretty sure that similar ones exist for other technologies/frameworks.

##1. I do not want to put backend information in my client application

Imagine the following scenario: you (client-side application) need to perform a couple of GETs to fetch some data that must be shown up in a page. How do you know which is the host address, the protocol and the port to call for such endpoint? Typically I have seen three approaches:

  • Backend information are put in the application at build time.
  • Backend information are passed to the web app as parameters or can be retrieved from environment variables.
  • The web app and the REST service are on the same machine: it can work because the web app can call localhost at a specific port and path. In that case we “only” need to hardcode port and protocol.

All strategies above lead you to a black hole when developing your web application:

  • You need to modify runtime status while debugging.
  • You need to hack the application in order to simulate the expected startup.
  • Worse of all, you need to point to a real shared dev/testing environment.

Reverse proxy The concept of reverse proxy it is quite easy. Let me present it like it was a black box feature. Suppose that someone configures the machine which is hosting the web app, so that when you call yourself (i.e. localhost) on a specific path (e.g. /api) every call is automatically forwarded to the API server; no matter which is the address, the protocol or the port in usage. That’s it. Nothing more. Now, if you want to jump inside the black box, here are a couple of articles that explain how to configure reverse proxy on:

Reverse proxy in Angular

Continuous Development - Building an Angular application

Scenario is described by the picture above. Suppose that your static files are served by the webpack dev server on the port 4200, while the APIs are served by a Node app on the port 3000. You can easily configure the global variable PROXY_CONFIG as part of webpack dev server lifecycle. You can choose to have proxy.conf.json or proxy.conf.js, depending on your angular.json configuration file. An example of PROXY_CONFIG below:

const PROXY_CONFIG = {
  "/api": {
    "target": "http://localhost:3000/",
    "secure": false,
    "logLevel": "debug",
    "changeOrigin": true
  }
};

module.exports = PROXY_CONFIG;

Every HTTP call must point to /api. No need to specify other information. The reverse proxy will do the rest for us:

getPosts(): Observable {
  return this.http.get('/api/posts/');
}

As soon as getPosts() is subscribed, the address http://localhost:3000/posts is called. You can learn more about reverse proxy in Angular in the official documentation:

##2. Able to code (even) without Internet connection:

When coding, the dependencies with the “outside world” should be as minimum as possible. You don’t want to be connected to a shared remote development machine, because:

  • It may not be available, since someone is updating it.
  • It may not be updated.
  • It may be slow. because of the its load.
  • It may be a lot of delay, because there is a VPN.
  • It may be unreachable, because your internet connection is not working properly.

Alternatively, you don’t want to launch locally a real instance of the development machine, because:

  • It may have third-party dependencies, that are difficult to mock.
  • It may be connected to a database, so are you going to install it or are you going to connect to a real remote instance? Nothing solved!
  • It may be difficult to update because your data are historical series, so what it is valid today, may be not valid tomorrow.
  • It may be heavy to run: for instance, 32GB of RAM are the minimum requirement to run.

Mocking data There are several solutions to make development fast and agile, for example, containers provide isolated and reproducible compute environments. What really makes sense, in my opinion, when working to a web app, it is the use of mocked APIs. If you are working with REST endpoints, I would suggest you JSON Server (https://github.com/typicode/json-server). json-server package can be installed both globally and so you can launch it anywhere you prefer, or you can install it as dev-dependency and create an NPM script to create a customized mocked server in your project. It is quite intuitive. You have a JSON as source of data, for example db.json:

{
  "posts": [
    { "id": 1, "title": "json-server", "author": "typicode" }
  ],
  "comments": [
    { "id": 1, "body": "some comment", "postId": 1 }
  ],
  "profile": { "name": "typicode" }
}

And you can launch it via command line:

json-server --watch db.json

By default, it starts on localhost, port 3000, so if you GET [http://localhost:3000/posts/1](http://localhost:3000/posts/1), you receive as response:

{ "id": 1, "title": "json-server", "author": "typicode" }

Other HTTP verbs can be used and you can choose to save edits in the original file or to leave it as is. Exposed APIs follow the REST standard, and you can sort, filter, paginate, load remote shemas, and as I mentioned earlier, you can create your own script and run an instance of JSON server programmatically:

const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()

server.use(middlewares)
server.use(router)
server.listen(3000, () => {
  console.log('JSON Server is running')
})

Mocking data in Angular Basically, I can suggest you a couple of strategies for making your Angular app working with mocked data. Both are based on the proxy:

  • Proxy would point http://localhost:3000/ in the target, so every call will point to such JSON server instance.
  • Proxy has custom mocking rule to return data for a specific path, using the bypass
parameter:
    const PROXY_CONFIG = {
      '/api': {
        'target': 'http://localhost:5000',
        'bypass': function (req, res, proxyOptions) {
          switch (req.url) {
            case '/api/json1':
              const objectToReturn1 = {
                value1: 1,
                value2: 'value2',
                value3: 'value3'
              };
              res.end(JSON.stringify(objectToReturn1));
              return true;
            case '/api/json2':
              const objectToReturn2 = {
                value1: 2,
                value2: 'value3',
                value3: 'value4'
              };
              res.end(JSON.stringify(objectToReturn2));
              return true;
          }
        }
      }
    }

module.exports = PROXY_CONFIG;

##3. Dev code should not affect Production code and vice versa

How many time have you seen something like this:

if (devMode) {...} else {...}

This is a code smell: you are mixing code for development purpose, with production one. A build targeted for production should not contain any code related to development, and vice versa. Different targets means different builds. There are a lot of use cases that can lead to this situation. For instance, your application could be behind SSO and the first time the user asks for your application (via browser), there is a redirect to an external page which asks for credentials. When you are in dev mode, you don’t want such redirect, so a less complex authentication service is welcome. File replacement in Angular In Angular, based on the current configuration, it is possible to specify a file replacement policy. For example, if the current configuration is production, then you can easily replace an authentication service (used for development purpose) with a more robust and complex one (used in production):

"configurations": {
  "production": {
    "fileReplacements": [
      {
        "replace": "src/app/core/services/authenticator.ts",
        "with": "src/app/core/services/authenticator.prod.ts"
      }
    ],
    ...
  ...
}

In this way, the codebase will have two separate services for the authentication, with two different purposes. And above all, in the final artifact, only one service will be included, based on the specific build parameter:

npm run ng build -c production

##4. Awareness about what is currently running in production

Are you able to identify every time, which version of your application is running on a certain host? There are few build information, like build time or last commit identifier, that help a lot to identify if your environment is updated “enough” to contain a certain fix. Build info in Angular There is a nice CLI, named angular-build-info (https://www.npmjs.com/package/angular-build-info) that produces a build.ts file inside your Angular projects src/ folder. You can then proceed to import this file inside your Angular application and use the exported buildInfo variable:

import { Component } from '@angular/core';
import { environment } from '../environments/environment';
import { buildInfo } from '../build';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor() {
    console.log(
      `\n%cBuild Info:\n` +
      `%c ❯ Environment: %c${environment.production ? 'production 🏭' : 'development 🚧'}\n` +
      ` ❯ Build Version: ${buildInfo.version}\n` +
      ` ❯ Build Timestamp: ${buildInfo.timestamp}\n`
    );  
  }
} 

In order to update the build.ts content (that must be versioned), remember to execute, at build time, the script below:

angular-build-info --no-message --no-user --no-hash

Obviously, parameters are optional, so you can customize produced buildInfo as you want.

#Conclusion

When you start working on a new project, non technical requirements should not slow down your productivity curve. The README.md of the repository related to such project should be enough to understand how to be ready to add value. When it’s too difficult to configure or when the dev machine sometimes works and sometimes no, there is an issue. Take care of these aspects. Happy developers have more time to spend on business logic, instead of technical impediments. Improvement of your developer experience is not a single-time process, but an incremental one: there is always room for automation; there is always room for improvements.