Splitting a angular project into multiple using same workspace - Part 1

angular
library
workspace

How to use angular workspace to create projects that have reusable code - Part 1


I recently ran into a small problem with the project I'm working on. The project is using Angular, and all access will be from desktop. But when the project was around 80% complete, 2 new requirements emerged:

  • There should be a mobile app for:
    • Perform 2-factor authentication;
    • Get the user's geolocation.

As the project is using Angular, we chose to use Ionic Framework in mobile app, because as it is possible to use angular with it, it would be better for code reuse.

But that's not what happened. Our application was made as just a mono-project, which is the default for angular. Any code we tried to reuse, we had to copy from one project to another, and that's not code reuse.

Then came the idea of creating a library for this project. Where all the reusable code (such as http services, guards, pipes, custom validators, etc.) would be in the library, which can be used by two or more projects simultaneously.

That's why the idea of posting about it came up. But I have so much to talk about... So I've decided to split it in small pieces that will be posted in the next few days.

In the first part, I'll talk about how to create the workspace, how to create the projects within the workspace, how to create a service within the library and how to use this service in the project.


Creating a Workspace

Let's start making sure we have the @Angular/CLI most recently version, then we create a workspace, without any project inside:

npm i -g @angular/cli
ng new <workspace name> --create-application false

Let's go into the workspace folder and use the CLI again, to generate the first application, which will be the workspace's default application.
We will also generate a library, which will be responsible for the HTTP requests of our project.

cd workspace
ng generate application app-name --routing --style=scss
ng generate library lib-name

Our file structure will be like this:

File structure

Each generated application will be a new folder inside the "projects" folder with its respective code, however, with the following difference between them:

  • Applications generated with "ng generate application" have the package.json shared between them.
  • Libraries generated with "ng generate library" have their own package.json.

Changing the Application

Let's modify our application and create a login page.
For this we will use the Angular CLI again and create a new component and its respective module:

ng generate module login --project=app-name --routing
ng generate component login --project=app-name

Change the login.component.html file to be like that:

<form [formGroup]="form" (ngSubmit)="login()">
  <div>
    <label>Username</label>
    <input type="text" formControlName="username"/>
  </div>

  <div>
    <label>Password</label>
    <input type="text" formControlName="password"/>
  </div>

  <button type="submit">
    Login
  </button>
</form>

login.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

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

  form!: FormGroup;

  constructor(
    public formBuilder: FormBuilder,
  ) { }

  ngOnInit(): void {
    this.form = this.formBuilder.group({
      username: null,
      password: null,
    })
  }

  login() {
    // ...
  }
}

Now that we have our simple login page, we need to have the service to authenticate.

Changing the library

Let's now modify our library.
First go to the library folder, which in our example is "lib-name", and inside the "src" folder delete everything leaving only the files public-api.ts and test.ts.
Now let's generate the module responsible for HTTP in our lib:

ng g module http --project=lib-name

And modify the http.module.ts file to look like this:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    HttpClientModule,
  ]
})
export class LibHttpModule { }

Let's also generate our AuthService:

ng generate service http/auth --project=lib-name

Let's use ReqRes as a mock API. Modify auth.service.ts to look like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpModule } from './http.module';

@Injectable({
  providedIn: HttpModule, // Atention here
})
export class AuthService {

  endpoint = 'https://reqres.in/api/login';
  
  constructor(
    public http: HttpClient
  ) { }

  login(username: string, password: string) {
    return this.http.post(this.endpoint, {
      username, password
    });
  }
}

Look at line 6, where we changed providedIn from root to our newly generated module HttpModule. That way, whoever needs to use the AuthService should import the HttpModule. This will be useful in later parts of this article.

Now modify public-api.ts to export the module and service:

/*
* Public API Surface of lib-name
*/

export * from './lib/http/http.module';
export * from './lib/http/auth.service';

We have our login component in a project, and the authentication service in the library.

Now we need to communicate between them.

Using the library into the application

First, we need to modify the tsconfig.json file from the root of our workspace. The paths property is responsible for mapping the directories, let's modify it to look like this:

{
  // ...
  "paths": {
    "lib-name/*": [
      "projects/lib-name/src/lib/*",
      "projects/lib-name/src/lib/"
    ],
    "lib-name": [
      "dist/lib-name/*",
      "dist/lib-name"
    ]
  },
  // ...
}

What do these settings do?
The first one (with "lib-name/*") is used during development. To make sure that every import of lib files will use the not compiled version.
The second (with just "lib-name") is used during the project's build time.

Now we can go back into the app-name project and modify the login.component.ts file to import and use the AuthService:

import { AuthService } from 'lib-name/http/auth.service';
// ...
export class LoginComponent implements OnInit {

  constructor( 
    public authService: AuthService
  ) { }
  // ...
  login() {
    const username = this.form.get('username')?.value;
    const password = this.form.get('password')?.value;
    
    this.authService.login(username, password).subscribe(
      response => console.log('success', response),
      error => console.log('error', error)
    );
  }
}

And login.module.ts to import our HttpModule:

// ...
import { HttpModule } from 'lib-name/http/http.module';

@NgModule({
  // ...
  imports: [
    // ...
    HttpModule,
  ]
})
export class LoginModule { }

And when we run our code, we'll see that the service is being called correctly:

Result

Code

The code write in this tutorial is available at:
https://github.com/higorcavalcanti/blog-code-angular-wordspace-part1

Other parts of this tutorial

You've finished reading part 1 of the series: "Splitting a angular project into multiple using same workspace - Part 1".
Soon there will be other parts.


References:

The Best Way To Architect Your Angular Libraries:
https://tomastrajan.medium.com/the-best-way-to-architect-your-angular-libraries-87959301d3d3

Building an Ionic Multi App Project with Shared Angular Library:
https://medium.com/angular-in-depth/building-an-ionic-multi-app-project-with-shared-angular-library-c9fa0383fd71

Angular docs: File Structure
https://angular.io/guide/file-structure#multiple-projects

Angular docs: Angular CLI
https://angular.io/cli