Build a full-stack web application with Angular 7 and Nest.js
Throughout this tutorial we'll be building a full-stack web application with Nest.js and Angular 7. We'll also introduce both frameworks and learn how to get started using each one of them.
The project we'll be building a simple contacts management application for managing a set of contacts in an Angular and Material Design interface retrieved from a Nest.js RESTful backend.
Prerequisites
Let's get started with listing the prerequisites you need to have for this tutorial. Basicaly, you will need to have:
- Both Node.js and NPM installed on your development machine. You can simply go to the official website and download the binaries designed for your operating system or you can check the official instructions for how to install the latest versions in your system. You can also check NVM (Node Version Manager) which is the recommended way for installing one or multiple versions of Node in your system without conflicts.
- Familiarity with TypeScript. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
If you have these prerequisites, you are good to go!
Introducing Angular
Angular is a frontend web framework for building mobile and desktop applications. It's built with TypeScript by Google and it's one of the most popular frameworks among frontend developers in the world. It allows developers to build applications for web, mobile web, native mobile and native desktop. It's makes use of libraries like RxJS and Immutable.js for efficiently working with huge data requirements.
Angular focuses on performance, speed and scalability and provides powerful tooling and features.
Angular follows a component-based architecture where an application is a tree of encapsulated and loosely coupled components with defined inputs and outputs. Code common between different components is encapsulated in services which are simply global singletons that get injected where they are needed via dependency injection.
Installing Angular CLI
The Angular CLI allows developers to easily generate Angular projects with the best practices. It also helps in generating various artifacts like components and services from the command line. You can also set up routing and Angular Material in your application using the ng add
command.
Open a new terminal and run the following command to install the Angular CLI from npm:
bash$ npm install -g @angular/cli
$$
Note: Please note that you may need to add
sudo
before your command in macOS and Linux or use a CMD prompt with administrator rights to install npm packages globally. If the commands fails, you can also fix your npm permissions.
If the installation is successful, you can invoke the ng
command to run various CLI commands from your terminal. For example let's display the installed version of the CLI:
bash$ ng version
$$
This is a screenshot of the output:
Image loading...
What is Nest.js
Nest is an open source MIT-licensed Node.js framework inspired by Angular for easily building efficient and scalable server-side applications. It's built with TypeScript and allows you to use the concepts of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).
Nest doesn't reinvent the wheel but makes use of the existing mature tools such as Express and TypeORM. On top of these tools, it provides an architecture that helps developers effortlessly build easily testable, scalable, loosely coupled, and maintainable web applications.
Installing Nest CLI
Nest CLI is a powerful command line interface that allows developers to generate a Nest.js project and the different artifacts like controllers and services. It also provides out of the box scripts for locally serving your project and building the final production-ready application that can be deployed on the web.
Nest CLI is based on the @angular-devkit package and provides its own schematics for Nest.js development.
Let's gets started with installing the Nest CLI. Open a new terminal and run the following command to install the CLI from npm:
bash$ npm install -g @nestjs/cli
$$
After finishing with the installation process, you will be able to use various CLI commands from your terminal. Let's see a first command:
bash$ nest --help
$$
This will display a list of the available commands at your disposal.
Creating our Angular and Nest Projects
After installing the required utilities, let's now proceed to create our project. Head back to your terminal and navigate to your working directory then create a folder for both the frontend and backend projects:
bash$ cd ~/demos $ mkdir nest-angular-app
$$$
Next, navigate to your created project and generate the backend and frontend projects using the following commands:
bash$ cd nest-angular-app $ ng new frontend $ nest new backend
$$$$
The Angular CLI will prompt you if you Would you like to add Angular routing? (y/N) Enter y( This will automatically set up routing in your project) and Which stylesheet format would you like to use? (Use arrow keys) Choose CSS and hit Enter.
The Nest CLI will ask you for a bunch of information about your project such as the name, description and author. Enter the required information or leave empty for the default values. The CLI will create the necessary files and then prompt you for which package manager to use to install the dependencies. Choose npm
.
Next, navigate to your frontend project and serve it using a live-reload development server using the following commands:
bash$ cd frontend $ ng serve
$$$
Your Angular application will be available from the 127.0.0.1:4200
address. If you navigate to that address using your browser you should be able to see the following page:
Image loading...
Open a new terminal and navigate to the backend project then run the development server using the following commands:
bash$ cd backend $ npm run start:dev
$$$
Your Nest application will be available from the 127.0.0.1:3000
address. If you go to that address with your web browser, you will get a blank page with a Hello World!:
Image loading...
Nest CLI makes use of nodemon which is an utility that will watch your source code for any changes and automatically reload your development server.
Now that we have both the frontend and backend apps up and running, let's start developing our full-stack contact management application.
Creating a MySQL Database and Setting up TypeORM
Open a new terminal and run the following command to invoke the mysql
client:
bash$ mysql -u root -p
$$
You will be prompted for a password. Enter the password you configured for your MySQL during the installation process.
Next, create a MySQL database:
bashmysql> create database nestngdb;
$$
Nest.js makes use of TypeORM which is the most mature Object Relational Mapper (ORM) available in TypeScript. It's available from the @nestjs/typeorm
package.
Navigate to your backend project and run the following commands to install the required dependencies to work with TypeORM and MySQL in your Nest.js project:
bash$ npm install --save @nestjs/typeorm typeorm mysql
$$
Next, you need to open the src/app.module.ts
file and import the TypeOrmModule
inApplicationModule
:
tsimport { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', database: 'nestngdb', username: 'root', password: 'YOUR_DATABASE_PASSWORD', entities: [__dirname + '/**/*.entity{.ts,.js}'], synchronize: true, }) ], controllers: [AppController], providers: [AppService], }) export class AppModule { }
We use the forRoot()
method of TypeOrmModule
which takes a configuration object similar to the one you would need to pass to the createConnection()
method of TypeORM.
Note: Make sure to replace
YOUR_DATABASE_PASSWORD
with your own MySQL database password.
In the configuration object we specify the following options:
- The
mysql
database as the type which tells TypeORM to connect to a MySQL database, - The
nestngdb
database we previously created, - The username and password for connecting to our MySQL server,
- The
entities
files (that end with.entity.ts
or.entity.js
extensions) which contain our ORM entities that will be mapped to SQL tables by TypeORM. - The
synchronize
option (which takestrue
orfalse
). If true, It tells TypeORM to automatically sync the database tables with the entities each time you run the app. This is helpful in development but not recommended in production.
Now that you have configured TypeORM in your project, you'll be able to inject the Connection
and EntityManager
services in your Nest.js services and controllers to work with the database.
Creating a Contact Entity
Next, let's create a Contact
entity that will be mapped to a contact
table in the MySQL database that will hold information about contacts in our application.
Create a src/entities/contact.entity.ts
file and add the following code:
tsimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Contact { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() title: string; @Column() email: string; @Column() phone: string; @Column() address: string; @Column() city: string; }
Next, you will need to import the Contact
entity and include it in the imports
array of the root module using the forFeature()
method. Open the src/app.module.ts
file and add the following changes:
ts// ... import { Contact } from 'entities/contact.entity'; @Module({ imports: [ /* ... */ TypeOrmModule.forFeature([Contact]), ], controllers: [AppController], providers: [AppService], }) export class AppModule { }
Once you save your file, the database table that corresponds to the Contact
entity will be created in your database.
Note: You can head to your mysql client and execute the
show tables;
instruction to make sure acontact
table is created.
Creating ContactService
Next, let's create a Nest service that contains the code for working with the contact
table. This service will basically contain the CRUD operations necessary for creating, reading, updating and deleting contacts from the database. Head back to your terminal and run the following command to generate a service:
bash$ nest generate service contact
$$
Next, open the src/contact.service.ts
file and add the following imports:
tsimport { Repository } from 'typeorm'; import { Repository, UpdateResult, DeleteResult } from 'typeorm'; import { Contact } from 'entities/contact.entity';
Next, inject the Contact
repository via the constructor:
ts@Injectable() export class ContactService { constructor( @InjectRepository(Contact) private contactRepository: Repository<Contact> ) { } }
Next, add the CRUD methods:
tsasync create(contact: Contact): Promise<Contact> { return await this.contactRepository.save(contact); } async readAll(): Promise<Contact[]> { return await this.contactRepository.find(); } async update(contact: Contact): Promise<UpdateResult> { return await this.contactRepository.update(contact.id,contact); } async delete(id): Promise<DeleteResult> { return await this.contactRepository.delete(id); }
Building the REST API
Let's now create the RESTful endpoints that our Angular frontend will be calling to retrieve and create data on the server. Head back to your terminal and run the following command to generate a controller:
bash$ nest generate controller contacts
$$
Next, open the src/contacts/contacts.controller.ts
file and import then inject ContactService
tsimport { Controller } from '@nestjs/common'; import { ContactService } from 'contact.service'; @Controller('contacts') export class ContactsController { constructor(private contactService: ContactService){ } }
You need to import the Get
, Post
,Put
, Delete
, Body
and Param
symbols and also the Contact
entity:
tsimport { Controller, Get, Post,Put, Delete, Body, Param } from '@nestjs/common'; import { Contact } from 'entities/contact.entity';
Finally, add the REST endpoints:
ts@Get() read(): Promise<Contact[]> { return this.contactService.readAll(); } @Post('create') async create(@Body() contact: Contact): Promise<any> { return this.contactService.create(contact); } @Put(':id/update') async update(@Param('id') id, @Body() contact: Contact): Promise<any> { contact.id = Number(id); return this.contactService.update(contact); } @Delete(':id/delete') async delete(@Param('id') id): Promise<any> { return this.contactService.delete(id); }
That's it! Our REST API is ready. At this point, we can use a REST API client like cURL or Postman to communicate with the API.
You can send a POST request to the /contacts/create
endpoint to create a contact:
Image loading...
You can also send a GET request to /contacts
endpoint to retrieve all created contacts:
Image loading...
Enabling CORS
Since we'll be communicating with our REST API from an Angular frontend running on another domain we need to enable CORS. Open the src/main.ts
file and call the enableCors()
method:
tsimport { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors(); await app.listen(3000); } bootstrap();
Implementing the Angular Interface
We have previously generated the Angular frontend and started the development server. Now, let's create the UI of our application.
Head back to a new terminal and run the following command to set up Angular Material:
bash$ ng add @angular/material
$$
When prompted to Choose a prebuilt theme name, or "custom" for a custom theme Choose Purple/Green:
Image loading...
Next, you will be prompted if you want to Set up HammerJS for gesture recognition? (Y/n) and Set up browser animations for Angular Material? (Y/n) You can choose the default answer which is Y.
That's it! Angular Material will be automatically installed and configured in your project.
After that, you need to import the Angular Material components that you want to use in your project. Open the src/app/app.module.ts
file:
ts// [...] import { MatInputModule, MatButtonModule, MatCardModule, MatFormFieldModule,MatTableModule } from '@angular/material'; @NgModule({ declarations: [ AppComponent, ContactComponent ], imports: [ // [...] MatTableModule, MatCardModule, MatInputModule, MatFormFieldModule, MatButtonModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
We'll be using the table, card, input, form field and button components. You can find the full list of components that you can use from the official docs.
Setting up HttpClient and Forms
We'll be using HttpClient
and a template-based form in our application so we need to import their modules. Open the src/app/app.module.ts
file and update it accordingly:
ts// [...] import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; @NgModule({ declarations: [ AppComponent ], imports: [ // [...] HttpClientModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Creating a Contact Model
Next, let's create a Contact
model. In your terminal, run the following command:
bash$ ng generate class contact
$$
Open the src/app/contact.ts
file and update it accodringly:
tsexport class Contact { id: number; name: string; title: string; email: string; phone: string; address: string; city: string; }
Creating an Angular Service
Next, let's create a service that contains the code for interfacing with the REST API. In your terminal, run the following command:
bash$ ng generate service api
$$
Next, open the src/app/api.service.ts
file and start by importing and injecting HttpClient
:
tsimport { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Contact } from './contact'; @Injectable({ providedIn: 'root' }) export class ApiService { constructor(private httpClient: HttpClient) { } }
We also imported the Contact
model.
Next, define the API_SERVER
variable that contains the address of the API server:
tsexport class ApiService { API_SERVER = "http://localhost:3000";
Next, add the CRUD operations:
tspublic readContacts(){ return this.httpClient.get<Contact[]>(`${this.API_SERVER}/contacts`); } public createContact(contact: Contact){ return this.httpClient.post<Contact>(`${this.API_SERVER}/contacts/create`, contact); } public updateContact(contact: Contact){ return this.httpClient.put<Contact>(`${this.API_SERVER}/contacts/${contact.id}/update`, contact); } public deleteContact(id: number){ return this.httpClient.delete(`${this.API_SERVER}/contacts/${id}/delete`); }
We use the get()
, post()
, put()
and delete()
methods of HttpClient
to send the GET, POST, PUT and DELETE requests to the corresponding endpoints of the API backend.
Creating the Contacts Component
Now, let's create the component where we can display our table of contacts and a form to create contacts.
In your terminal, run the following command:
bash$ ng generate component contact
$$
Next, open the src/app/app-routing.module.ts
file and add a route for accessing the component:
tsimport { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { ContactComponent } from './contact/contact.component'; const routes: Routes = [ {path: "", pathMatch: "full", redirectTo: "contacts"}, {path: "contacts", component: ContactComponent} ]; // [...]
We add a /contacts
route mapped to ContactComponent
and a route to redirect the empty path to the /contacts
path.
Next open the src/app/contact/contact.component.ts
and add the following imports:
tsimport { ApiService } from '../api.service'; import { Contact } from '../contact';
Next inject ApiService
and declare the following variables:
tsexport class ContactComponent implements OnInit { displayedColumns : string[] = ['id', 'name', 'title', 'email', 'phone', 'address', 'city', 'actions']; dataSource = []; contact = {}; constructor(private apiService: ApiService) { }
The displayedComumns
variable holds the name of the columns that will be displayed in the Angular Material Table we'll be adding later. The dataSource
variable will contain the data of the table and the contact
variable will contain the selected contact from the table.
Next, retrieve the contacts from the server when the component is initialized:
tsngOnInit() { this.apiService.readContacts().subscribe((result)=>{ this.dataSource = result; }) }
We simply call the readContacts()
method of ApiService
in the ngOnInit()
method of the component and we subscribe to the returned RxJS Observable then we assign the result to the dataSource
variable.
Next, add the selectContact()
and newContact()
methods:
tsselectContact(contact){ this.contact = contact; } newContact(){ this.contact = {}; }
The selectContact()
method assigns the selected contact to the contact
variable and the newContact()
method assigns an empty object to the contact
variable. These two methods will be bound to a select and new buttons that will be adding later in our UI.
Finally, add the other CRUD methods to create, update and delete contacts:
tscreateContact(f){ this.apiService.createContact(f.value).subscribe((result)=>{ console.log(result); }); } deleteContact(id){ this.apiService.deleteContact(id).subscribe((result)=>{ console.log(result); }); } updateContact(f){ f.value.id = this.contact['id']; this.apiService.updateContact(f.value).subscribe((result)=>{ console.log(result); }); }
These methods are simply wrappers of the corresponding methods in ApiService
.
Creating the Template
After adding the code for the component, we now need to create the template. Open the src/app/contact/contact.component.html
and start by adding a Material Design table that will display the contacts:
html<mat-card> <mat-card-header> <mat-card-title>Contacts</mat-card-title> </mat-card-header> <mat-card-content> <table mat-table [dataSource]="dataSource" class="mat-elevation-z8" style="width: 100%;"> <ng-container matColumnDef="id"> <th mat-header-cell *matHeaderCellDef>ID </th> <td mat-cell *matCellDef="let element"> {{element.id}} </td> </ng-container> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef>Name </th> <td mat-cell *matCellDef="let element"> {{element.name}} </td> </ng-container> <ng-container matColumnDef="title"> <th mat-header-cell *matHeaderCellDef> Title </th> <td mat-cell *matCellDef="let element"> {{element.title}} </td> </ng-container> <ng-container matColumnDef="email"> <th mat-header-cell *matHeaderCellDef> Email </th> <td mat-cell *matCellDef="let element"> {{element.email}} </td> </ng-container> <ng-container matColumnDef="phone"> <th mat-header-cell *matHeaderCellDef> Phone </th> <td mat-cell *matCellDef="let element"> {{element.phone}} </td> </ng-container> <ng-container matColumnDef="address"> <th mat-header-cell *matHeaderCellDef> Address </th> <td mat-cell *matCellDef="let element"> {{element.address}} </td> </ng-container> <ng-container matColumnDef="city"> <th mat-header-cell *matHeaderCellDef> City </th> <td mat-cell *matCellDef="let element"> {{element.city}} </td> </ng-container> <ng-container matColumnDef="actions"> <th mat-header-cell *matHeaderCellDef> Actions </th> <td mat-cell *matCellDef="let element"> <button mat-raised-button (click)="deleteContact(element.id)" color="secondary">Delete Contact</button> <button mat-raised-button (click)="selectContact(element)" color="secondary">Select Contact</button> </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table> </mat-card-content> </mat-card>
You can find more details of how you create a Material table from the official docs.
Next, let's add a form to create and update contacts just below the table:
html<mat-card> <mat-card-header> <mat-card-title>Create a Contact</mat-card-title> </mat-card-header> <mat-card-content> <form #f ="ngForm" class="mat-elevation-z8" style="width: 100%; padding: 5px;"> <mat-form-field> <input matInput placeholder="Name" name="name" [(ngModel)] = "contact.name" required> </mat-form-field> <mat-form-field> <input matInput placeholder="Title" name="title" [(ngModel)] = "contact.title" required> </mat-form-field> <mat-form-field> <input matInput placeholder="Email" name="email" [(ngModel)] = "contact.email" required> </mat-form-field> <mat-form-field> <input matInput placeholder="Phone" name="phone" [(ngModel)] = "contact.phone" required> </mat-form-field> <mat-form-field> <textarea placeholder="Address" name="address" [(ngModel)] = "contact.address" matInput></textarea> </mat-form-field> <mat-form-field> <input matInput placeholder="City" name="city" [(ngModel)] = "contact.city" required> </mat-form-field> </form> </mat-card-content> <mat-card-actions> <button mat-raised-button *ngIf="!contact.id" (click)="createContact(f)" color="primary">Save Contact</button> <button mat-raised-button *ngIf="contact.id" (click)="updateContact(f)" color="primary">Update Contact</button> <button mat-raised-button (click)="newContact()" color="primary">New..</button> </mat-card-actions> </mat-card>
We simply create a template-based table with the fields for entering information about the contact to create or update. We also add buttons for creating, updating, selecting contacts and emptying the form.
Next, open the src/app/app.component.html
and change accordingly:
html<div style="text-align:center"> <h1> Contact Management </h1> </div> <router-outlet></router-outlet>
We add a title and the router outlet where the router will insert the matched component.
This is a screenshot of the page after adding two contacts:
Image loading...
This is a screenshot of the page after selecting a contact:
Image loading...
Conclusion
Throughout this tutorial we've used Nest.js and Angular 7 to create a full-stack REST API web application from scratch. We have also used Angular Material to build our application UI. You can find the source code from this GitHub repository.