Creating Multi-Tenant Sites with Angular

Do you want to build a single Angular application that can be used for multiple clients? This is often referred to as a multi-tenant application. I'll show you how to build such an app in this article.

Angular

The app we will build shall consist of a landing page that displays a welcome message that is customized for the client. We will also create a client details component that displays more information about the client.

I'll be using VS Code and Angular CLI for the development of this app. I'm assuming familiarity with Angular so we are going to jump right into coding the app. Also, it's important to know this app was built with angular 7.2.0. Most of the code is backwards compatible but you'll need to make some tweaks if you are on earlier versions.

Create your new app

From the command line, execute the following command to create a new app:

ng new multi-tenant-app
  • When prompted to add routing, answer Yes.
  • When prompted for stylesheet format, choose Sass

In just a few moments, you will have a brand new shiny Angular app:
App outline

Some minor housekeeping

Open up app.component.html and delete all of the contents except the router outlet:

<router-outlet></router-outlet>

Create a landing page

Let's create a landing page which we will call the welcome component:

ng generate component welcome

The welcome page will expect to be able to get the id of the client from the route parameters. We'll adjust our routing momentarily but in the meantime, open up welcome.component.ts and place within it the following code:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.component.html',
  styleUrls: ['./welcome.component.sass']
})
export class WelcomeComponent implements OnInit {

  clientId: string;
  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.clientId = this.route.snapshot.paramMap.get('client');
  }

}

The code grabs a parameter named client from the route and stores it within clientId. Next, open up welcome.component.html and add the following code:

<p>
  Welcome {{clientId}} !!!
</p>

Create specific urls for each client

Now that we have a simple welcome component that can display a parameter that it obtains from a route, let's adjust our routing within our app so we can accomodate this parameter. Open up app-routing.module.ts and update the routes array to look as follows:

const routes: Routes = [
  {
    path: ':client',
    component: WelcomeComponent
  }
];

Run the app

Let's go ahead and run the app and see what it does. Execute the following command:

ng serve

When the app is ready, open your browser to localhost:4200. You should see a blank browser page. Don't worry, this is expected at this point and we will address this in a moment. For now, see what happens if you adjust the url by appending a parameter representing a client - give it a try, for example:
welcome

As you can see, the client parameter is displayed in the page as a welcome message. Now that we have the ability to route to a welcome page and display something specific for a client, let's build on this. Here are the enhancements we will make:

  • Define a list of clients that can use the app
  • If a client is not in the list, redirect to a specific 'client-not-found' page.

Define our list of clients

To make it easy to manage a list of clients that can use our app, let's create a json file that will hold the list. When we want to change the list, it is just a simple deployment of a json file. Create a new file under assets and call it clients.json. Here is the contents of clients.json:

{
    "clients": [
        {
            "id": "ibm",
            "name": "IBM",
            "website": "ibm.com"
        },
        {
            "id": "angular",
            "name": "Angular Framework",
            "website": "angular.io"
        },
        {
            "id": "google",
            "name": "Google (alphabet)",
            "website": "google.com"
        }
    ]
}

Since we are going to load this list into the app at runtime, we need a model to represent a client. Create a new folder under app and call it models. Under this new folder create a new file called client.ts. Here is the code for client.ts:

export class Client {
    id: string;
    name: string;
    website: string;
}

Now that we have our list, let's put it to use.

Enforcing our list

Earlier in this article we showed you how to add a parameter to the url of this app so that we can identify a client. Let's take that parameter and match it up against our list. If the client is not found we will forward the user to a 'not-found' page. To do this we are going to make use of route guards. Let's create a new route guard. Open up a terminal window and execute the following command:

ng generate service client-guard

Next, open up client-guard.service.ts that was just generated. It's in here that we will put our list of clients to use. We'll check our client parameter against this list and prevent access to the route if the client was not found. Here is the code:

import { Injectable } from '@angular/core';
import clientsData from '../assets/clients.json';
import { Client } from './models/client';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, CanActivate, UrlTree } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class ClientGuardService implements CanActivate {

  private clients: Client[] = clientsData.clients;

  constructor(private router: Router) { 
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree
  {    
    let id: string = route.params['client'];
    if(!id) {
      return this.router.parseUrl('');
    }
    let found: Client = this.clients.find(x => x.id.toLowerCase() == id.toLowerCase());
    if(!found) {
      return this.router.parseUrl('');
    }
    else {
      return true;
    }
  }

}

At this point if you were to try and build, you would get a compiler error that is similar to: "Cannot find module '../assets/clients.json'. Consider using '--resolveJsonModule' to import module with '.json' extension".

To resolve this, open up your tsconfig.json and add the following two lines near the end:

    "resolveJsonModule": true,
    "esModuleInterop": true

Your app should build fine now, just execute: ng build

Angular's route guards are easy to add to existing routes. Open up your app-routing.module.ts file and modify the routes to look as follows:

const routes: Routes = [
  {
    path: ':client',
    component: WelcomeComponent,
    canActivate: [ClientGuardService],
  }
];

Also, don't forget to import ClientGuardService into app-routing.module.ts:

import { ClientGuardService } from './client-guard.service';

Going back to the ClientGuardService, you will see that we are redirecting the user to the root url of the app whenever a client parameter is specificed that is not in our list. To make this work, we need to generate a component and update our routing:

ng generate component client-not-found --dry-run

Next, open up client-not-found.component.html and change the code as follows:

<p>
  Sorry, this is not a valid client
</p>

Now, we need to again update our routing again. Open up app-routing.module.ts and add in the following route to the Routes array:

  {
    path: '**',
    component: ClientNotFoundComponent
  }

The '**' is a fallback route that is going to be used whenever any other route is not matched or is prevented by route guards. So what we end up having at this point is an app that if the client parameter is not in a pre-built list, we prevent navigation and send the user to a page notifying them of the problem. Feel free to build again and serve up the app and see how it works!

Adding a client details component

Let's create a component that will dispay additional details about the client. A user can view the details by clicking on a link that we will add to the landing page.

Create the client details component

Execute the following command:

ng generate component client-details

Update your routes to support the client details page

Open up app-routing.module.ts and update the first route to looks as follows:

  {
    path: ':client',
    component: WelcomeComponent,
    canActivate: [ClientGuardService],
    children: [
      {
        path: 'detail',
        component: ClientDetailsComponent
      }
    ]
  },

What this does is add in a child route under the 'client' route so that we can match the url pattern :client/detail where :client is a placeholder for a client that is contained within your clients.json list.

Display the client details

To get this working we need to modify the welcome.component.html to render the details component. We do this by adding in a navigation link and a router outlet. Your entire source for the welcome.component.html should now looks as follows:

<p>
  Welcome {{clientId}} !!!
</p>
<a routerLink="detail" routerLinkActive="active">View Details</a>
<router-outlet></router-outlet>

With the plumbing for this in place, let's turn our attention to actually displaying the client details. Open up the client-details.component.ts file. We'll add some code to get the parent route's client id parameter, then look inside the clients.json list and pull out the relevant details:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Client } from '../models/client';
import clientsData from '../../assets/clients.json';

@Component({
  selector: 'app-client-details',
  templateUrl: './client-details.component.html',
  styleUrls: ['./client-details.component.sass']
})
export class ClientDetailsComponent implements OnInit {

  private clients: Client[] = clientsData.clients;
  clientId: string;
  client: Client;
  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    let id = this.route.parent.snapshot.paramMap.get('client');
    this.client = this.clients.find(x => x.id.toLowerCase() == id.toLowerCase());
  }

}

And one last modification needed to render the client details on the screen. Open up client-details.component.html and modify it to look as follows:

<p>
  Name: {{client.name}}
</p>
<p>
  Website: {{client.website}}
</p>

Now, run the app and see what happens. You should see a link on the welcome page that says view details. Clicking the link shows the client details!
details

NOTE: there are opportunities for improvement here. The code between the client-guard.service.ts and the client-details.component.ts is very similar. Abstracting out the logic to find a client in a list is better suited for service work. Try creating a service on your own and refactor for extra credit! 🙂

Summary

As you can see, by employing Angular routing parameters, some route guards, and the ability to easily import json data, its rather straight-forward to build a single application in Angular that can be used by multiple companies.

Leave a Reply

Your email address will not be published. Required fields are marked *