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

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