Code-sharing made easy in a full-stack app with Nx, Angular, and NestJS

Post Editor

In this article, we'll combine Angular and NestJS while building a journal app and learn how to take advantage of code sharing in a Nx monorepo

8 min read
post

Code-sharing made easy in a full-stack app with Nx, Angular, and NestJS

In this article, we'll combine Angular and NestJS while building a journal app and learn how to take advantage of code sharing in a Nx monorepo

post
post
8 min read
8 min read

When building a full-stack application it is desirable to avoid code duplication. However, this can be challenging when you have to share some logic or types between frontend and backend.

In this article, I will show how this can be very simple to use an Nx monorepo. We will build a simple Journal app in Angular and connect it with an API in NestJS. I've chosen to use these frameworks since both make use of TypeScript and they fit very well with what we are trying to accomplish.

However, these same instructions could easily be adapted for other frameworks like React and NodeJS since they are also supported in Nx monorepos.

Create an Nx Workspace

Let’s start by creating an Nx Workspace. This can be achieved by running the following command:

$ npx create-nx-workspace trombonix --preset=empty --cli=angular

Here trombonix is the name of our workspace, we use the preset empty which scaffolds no apps or libs initially. Since we are building an Angular application, we decided also to use  Angular CLI in our workspace. Now if we cd into the trombonix folder:

$ cd trombonix

We can see that an empty project was scaffolded:

trombonix
├── apps
├── dist
├── libs
├── node_modules
├── tools
├── README.md
├── angular.json
├── jest.config.js
├── nx.json
├── package.json
├── tsconfig.json
├── tslint.json
└── yarn.lock

Our workspace is empty and we can add the technologies we want to use. In this project, we’re building a full-stack application where the journal app will be written using Angular and the API using NestJS, therefore we need to install these schematics to our workspace by running:

$ ng add @nrwl/nest --defaults

$ ng add @nrwl/angular --defaults

The --defaults option tells the schematic to use the default options, which in this case means it will default to Jest and Cypress as unit test and E2E test suites respectively. This aspects will not be covered in this article.

Now everything is set for us to start creating our journal full-stack application.

Create the NestJS API

If you’re familiar with Angular syntax, the angularish coding style of NestJS will make it easy to understand what is going on here. Let's create an API by using the following command:

$ ng generate @nrwl/nest:app api --directory

Here we use the option --directory to indicate that we want the API to be in the root of our apps folder. The folder structure looks like this:

apps/api
├── src
│   ├── app
│   │   ├── app.controller.spec.ts
│   │   ├── app.controller.ts
│   │   ├── app.module.ts
│   │   ├── app.service.spec.ts
│   │   └── app.service.ts
│   ├── assets
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   └── main.ts
├── jest.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json

Let’s update app.service.ts to implement the basic get, save, and delete functions. For simplicity, we’ll implement an in-memory persistence.

import { Injectable } from '@nestjs/common';

export interface JournalEntry {
  title: string;
  body: string;
  timestamp?: Date;
}

@Injectable()
export class AppService {

  entries: JournalEntry[] = [{
     title: 'example title',
     body: 'example journal entry',
    timestamp: new Date()
  }];

  getData(): JournalEntry[] {
    return this.entries;
  } 

  create(entry: JournalEntry) {
    const newEntry = {
      title: entry.title,
      body: entry.body,
      timestamp: new Date()
    };   

    this.entries = [...this.entries, newEntry]; 
  } 

  delete(id: number) {
    this.entries = this.entries.filter((_, idx) => idx !== id); 
  }
}

Then we need to update app.controller.ts to expose the verbs GET, POST, and DELETE in our entries endpoint.

import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';

import { AppService, JournalEntry } from './app.service';

@Controller('entries')
export class AppController {
 constructor(private readonly appService: AppService) {}

 @Get()
 getData() {
   return this.appService.getData();
 }

 @Post()
 create(@Body() body: JournalEntry) {
   return this.appService.create(body);
 }

 @Delete(':id')
 delete(@Param('id', ParseIntPipe) id: number) {
   return this.appService.delete(id);
 }
}

That’s it! To run our API and check the results we use:

$ ng serve api

Now if we navigate to http://localhost:3333/api/entries , we can see that the example entry is returned.

Create the Journal App

This command uses the Nrwl schematics for Angular to create an app following the workspace best practices. Let's create our journal app:

$ ng generate @nrwl/angular:app journal --routing=false --style=scss --backend-project=api

For this command we set backend-project=api. This creates a proxy.conf.json in our app, which sets the Angular proxy to redirect the requests made to  api to our API avoiding problems with CORS.

apps/journal
├── src
│   ├── app
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   └── app.module.ts
│   ├── assets
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   └── test-setup.ts
├── browserslist
├── jest.config.js
├── proxy.conf.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json

In order to make our UI look nicer, we'll add a small CSS library, that I love, called Bulma. The installation is very simple, we just need to install the package:

$ yarn add bulma

And add the import in our styles.scss file:

@import 'bulma';

body {
  height: 100vh;
  background-color: #fcfcfc;
}

Since we're communicating with an API, let's create a angular service to hold our HTTP requests. We can use the Angular CLI to scaffold it for us:

$ ng generate @nrwl/angular:service services/data --project=journal

Let's also add the HttpClientModule to the root module of our app at app.module.ts

import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now we need to connect our service to our API using the HttpClient:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

export interface JournalEntry {
  title: string;
  body: string;
  timestamp?: Date;
}

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) {}

 fetch() {
   return this.http.get<JournalEntry[]>('/api/entries');
 }

 save(entry: JournalEntry) {
   return this.http.post('/api/entries', entry);
 }

 delete(id: number) {
   return this.http.delete(`/api/entries/${id}`);
 }
}

Note that we duplicated the JournalEntry interface here, in order to safely type our response, that's okay for now. We'll see how to avoid this duplication in the next section.

With our service in place we can code a simple component that uses the data.service.ts to read and register entries in our journal. We can start with app.component.ts:

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

import { DataService, JournalEntry } from './services/data.service';

@Component({
 selector: 'trombonix-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
 entries: JournalEntry[];

 constructor(private dataService: DataService) {}

 ngOnInit(): void {
   this.fetch();
 }

 fetch() {
   this.dataService.fetch().subscribe({
     next: (response: JournalEntry[]) => (this.entries = response)
   });
 }

 onSaveEntry(titleInput: HTMLInputElement, bodyInput: HTMLInputElement) {
   const entry = {
     title: titleInput.value,
     body: bodyInput.value
   };
   this.dataService.save(entry).subscribe({
     next: () => {
       this.fetch();
       titleInput.value = '';
       bodyInput.value = '';
     }
   });
 }

 onDeleteEntry(index: number) {
   this.dataService.delete(index).subscribe({
     next: () => {
       this.fetch();
     }
   });
 }
}

Then add some UI by changing the template in  app.component.html:

<h1 class="title">One line Journal</h1>

<input #titleInput class="input is-fullwidth" placeholder="Title" />

<input
 #bodyInput
 class="input is-fullwidth"
 placeholder="Start typing here..."
 (keydown.ENTER)="onSaveEntry(titleInput, bodyInput)"
/>

<button
 class="button is-info"
 type="submit"
 (click)="onSaveEntry(titleInput, bodyInput)"
>
 save
</button>

<div class="card" *ngFor="let entry of entries; index as idx">
 <div class="card-content">
   <h1 class="title">{{ entry.title }}</h1>
   <button class="delete is-small" (click)="onDeleteEntry(idx)"></button>
   <p>"{{ entry.body }}"</p>
   <p class="is-size-7 has-text-grey-lighter">
     {{ entry.timestamp | date: 'short' }}
   </p>
 </div>
</div>

and adding some simple CSS classes in app.component.scss

:host {
 display: block;
 font-family: sans-serif;
 min-width: 300px;
 max-width: 600px;
 padding: 50px;
 margin: auto;
}

input {
 margin-bottom: 8px;
}

.card {
 margin: 16px;
 border-radius: 8px;
}

.delete {
 position: absolute;
 right: 8px;
 top: 8px;
}

The journal app is ready! We can check the results by running:

$ ng serve journal -o

Great! It works! Now we can write our daily memos and have them persisted in memory by our API.

Create a shared library

You might have noticed that to safely type everything it was needed to duplicate a JournalEntry interface in both the journal app and the API. Let’s create a shared library to extract this common code and avoid the code duplication.

Creating a shared library is simple using the command below:

$ ng generate @nrwl/workspace:library types 

The types library was created inside the libs directory:

libs
└── types
    ├── src
    │   ├── lib
    │   └── index.ts
    ├── README.md
    ├── jest.config.js
    ├── tsconfig.json
    ├── tsconfig.lib.json
    ├── tsconfig.spec.json
    └── tslint.json

Now let’s move the interface to types.ts and refactor our code to use the shared type:

export interface JournalEntry {
  title: string;
  body: string;
  timestamp?: Date;
}

Next we remove the interface from the API in the app.service.ts file:

import { Injectable } from '@nestjs/common';
import { JournalEntry } from '@trombonix/types'; // <-- this should be added instead

// this should be removed
// export interface JournalEntry {
//   title: string;
//   body: string;
//   timestamp?: Date;
// }

@Injectable()
export class AppService {
...
}

Notice that we can now import the JournalEntry from @trombonix/types, which points to the shared types library. This is possible thanks to the schematic we used to scaffold the library. It creates a TypeScript alias for the types library.

We also need to correct the imports on the app.controller.ts to import from our shared types library. The same process has to be repeated for data.service.ts and app.component.ts in the Journal app respectively.

We’ve completed our full-stack Journal app with a shared library. If we generate a dependency graph, we can see how our code is connected. This can be achieved by using once more the CLI:

 $ yarn dep-graph

As we see above, now we have a shared library types that contains the types used by the frontend and also the backend. This allows us to define a contract between both parts while developing and take maximum advantage of TypeScript.

Conclusion

In this article we covered how to leverage the NxDevTools to create a full-stack application. Furthermore, we learned how to create a shared library to avoid code duplication and make our code more maintainable

Be aware that not all the code is a good candidate to be shared between frontend and backend. The response interfaces are a good choice, since they create a contract between backend and frontend. However, classes with too much business logic can create tight coupling and should be used sparingly.

An additional advantage of using a monorepo is the ability to develop a feature end-to-end without the need to synchronize multiple pull requests in different repositories.

An Nx workspace comes packed full of tooling that makes developing in a monorepo practical and productive. In this article, we barely scratched the surface of the potential behind these tools, but I hope it inspired you to start exploring them.

The full code for this article can be found in GitHub: https://github.com/Carniatto/journal-nx-angular-nest

Discuss with community

Share

About the author

author_image

Angular Architect at Riaktr, Core Team for NG-BE. HackYourFuture BE Coach. Writing and Speaking about Angular, NGXS, NxDevTools, and NestJs

author_image

About the author

Mateus Carniatto

Angular Architect at Riaktr, Core Team for NG-BE. HackYourFuture BE Coach. Writing and Speaking about Angular, NGXS, NxDevTools, and NestJs

About the author

author_image

Angular Architect at Riaktr, Core Team for NG-BE. HackYourFuture BE Coach. Writing and Speaking about Angular, NGXS, NxDevTools, and NestJs

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
4 March 20218 min read
Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Angularpost
4 March 20218 min read
Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Read more
AngularpostAngular Universal: real app problems

4 March 2021

8 min read

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Read more
Angularpost
3 March 20215 min read
View State Selector  - Angular design pattern

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Angularpost
3 March 20215 min read
View State Selector  - Angular design pattern

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Read more
AngularpostView State Selector  - Angular design pattern

3 March 2021

5 min read

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Read more
RxJSpost
26 February 20213 min read
RxJS: Why memory leaks occur when using a Subject

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

RxJSpost
26 February 20213 min read
RxJS: Why memory leaks occur when using a Subject

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

Read more
RxJSpostRxJS: Why memory leaks occur when using a Subject

26 February 2021

3 min read

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

Read more