I recently wrote a tutorial titled,Ā Creating a User Profile Store with Node.js and a NoSQL Database, as it is a very popular subject and use-case when it comes to NoSQL databases. In that tutorial, we created an API using Node.js and Couchbase for storing users, information associated with particular users, and expiring sessions. However, it was strictly backend related with no user interface.
We wanted to use the user profile store API and understand how to create, using TypeScript and Angular, a profile page that communicates with each of the API endpoints.
This tutorial has been updated as of January 7th, 2021 by Eric Bishard to work with Angular 11!
Before continuing, we are assuming that you’ve followed the previous tutorial. We’re also going to assume that you have the Angular CLI installed.
Create an Angular Project With the Necessary Components
The client front-end will be rudimentary, but we will build it from scratch. From a design perspective, it isn’t going to look great because this tutorial is all about functionality and working with our API, it’s up to you if you want to make it look nice and provide better UX.
From the Angular CLI, execute the following command:
1 |
ng new couchbase-angular-blog-app |
The above command will create a new project in the CLI path. With the project created, we’ll need to create a component for each of our expected application pages.
Execute the following with the Angular CLI:
1 2 3 4 |
ng g component login ng g component register ng g component blogs ng g component blog |
Four Angular user profile components will be created, each with appropriate TypeScript and HTML files. Before we start adding logic to them, we need to tie them together for navigation purposes using the angular user profile template below.
Open the project’sĀ src/app/app.module.ts file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { LoginComponent } from './login/login.component'; import { RegisterComponent } from './register/register.component'; import { BlogsComponent } from './blogs/blogs.component'; import { BlogComponent } from './blog/blog.component'; import { RouterModule } from '@angular/router'; let routes = [ { path: '', redirectTo: '/login', pathMatch: 'full' }, { path: 'login', component: LoginComponent }, { path: 'register', component: RegisterComponent }, { path: 'blogs', component: BlogsComponent }, { path: 'blog', component: BlogComponent } ]; @NgModule({ declarations: [ AppComponent, LoginComponent, RegisterComponent, BlogsComponent, BlogComponent ], imports: [ BrowserModule, FormsModule, HttpClientModule, RouterModule, RouterModule.forRoot(routes) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } |
The above code was created by the Angular CLI, however, we added a few core and custom module imports and imported them along with the routes
into the @NgModule
block.
To activate our navigation, we’ll need to update a file. Open the project’sĀ src/app/app.component.html file and replace all the content with the following:
1 |
<router-outlet></router-outlet> |
At this point, the user profile project has a basic configuration with angular.
Let’s have a look at handling the creation of user profiles and signing in with the account information.
Handling Login and Registration
The first screen the user will see is the login screen. The purpose here is to collect a username and password, send it to the API, get a session id as a response, and use it on every future page of the application.
Open the project’sĀ src/app/login/login.component.ts file and include the following TypeScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import { Component, OnInit } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Router } from '@angular/router'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { public input: any; constructor(private http: HttpClient, private router: Router) { this.input = { 'email': '', 'password': '' }; } ngOnInit(): void { } public login() { if (this.input.email && this.input.password) { let headers = new HttpHeaders({ 'content-type': 'application/json' }); this.http.post('http://localhost:3000/login', JSON.stringify(this.input), { headers: headers }) .subscribe(result => this.router.navigate(['/blogs'], { 'queryParams': result }) ); } } } |
There are some important things happening in the above code so we’re going to break it down.
Our intention is to bind an object to a form in the HTML markup. This object will be the input
variable. In the constructor
method we set each possible property as an empty value which will be reflected in the UI.
When the user decides to sign in, the appropriate header information is set for the request and the object is sent to the server. If successful, the response will be an object with the session id. We’re going to pass it to the next page as a query parameter.
The HTML markup that pairs with this TypeScript is found in the project’sĀ src/app/login/login.component.html and it looks like the following:
Sign In
1 |
A note I should mention here is that is you type in the wrong password we have not created any way of letting the user know that the username or password used was not correct, again, functionality only here and you will have to create that functionality as it is a little beyond our basic demo.
What is important here is the use of the [(ngModel)]
attributes that are used for data binding. We also offer navigation to the registration page through theĀ [routerLink]
attribute.
So what does the registration component look like?
Open the project’sĀ src/app/register/register.component.ts file and include the following TypeScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import { Component, OnInit } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Router } from '@angular/router'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.css'] }) export class RegisterComponent implements OnInit { public input: any; public constructor(private http: HttpClient, private router: Router) { this.input = { 'firstname': '', 'lastname': '', 'email': '', 'password': '' }; } ngOnInit(): void { } public register() { if (this.input.email && this.input.password) { let headers = new HttpHeaders({ 'content-type': 'application/json' }); this.http.post('http://localhost:3000/account', JSON.stringify(this.input), { headers: headers }) .subscribe(() => this.router.navigate(['/login']) ); } } } |
You’ll notice that pretty the registration code is similar to the login code. We’re just collecting form data and sending it to a different API endpoint.
The corresponding HTML found in the project’sĀ src/app/register/register.component.html file looks like this:
Register
1 |
The HTML contains form elements bound with the [(ngModel)]
attribute as well as a link back to the login page.
Remember, after we’ve signed in we are passing the session to the user-specific pages. This will allow us to get our user information from our profile store.
Creating and Viewing Blog Articles for a Particular User in the Profile Store
After signing in, the user is taken to a page where they can view a list of blog articles that they’ve written. Remember, our backend is connecting NoSQL documents through a profile id that we’re defining.
To view blog articles, the session id passed from the previous page needs to be set as a header in a request. The result from the request can be immediately rendered to the screen.
Open the project’sĀ src/app/blogs/blogs.component.ts file and include the following TypeScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { Component, OnInit } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-blogs', templateUrl: './blogs.component.html', styleUrls: ['./blogs.component.css'] }) export class BlogsComponent implements OnInit { private sid: string; public entries: any; public constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute ) { this.entries = []; } public ngOnInit() { this.route.queryParams.subscribe(params => { this.sid = params['sid']; let headers = new HttpHeaders({ 'authorization': 'Bearer ' + params['sid'] }); this.http.get('http://localhost:3000/blogs', { headers: headers }) .subscribe(result => { this.entries = result; console.log(result) }); }); } public create() { this.router.navigate(['/blog'], { 'queryParams': { 'sid': this.sid } }); } } |
This file follows a similar strategy to how we handled login and registration.
In terms of variables, we create a private variable called sid
which will hold the session passed from the previous page. The entries
variable will be an array of blog entries for an individual returned from the API endpoint.
When the page loads we should be displaying the blog entries. It is never a good idea to load or request data from within the constructor
method, so instead we use the ngOnInit
method.
1 2 3 4 5 6 7 8 9 10 11 |
public ngOnInit() { this.route.queryParams.subscribe(params => { this.sid = params['sid']; let headers = new HttpHeaders({ 'authorization': 'Bearer ' + params['sid'] }); this.http.get('http://localhost:3000/blogs', { headers: headers }) .subscribe(result => { this.entries = result; console.log(result) }); }); } |
In the ngOnInit
method we can grab the passed parameters and construct an authorization header. Then we make a request against our endpoint that contains the header.
To create a new blog entry, we can pass the sid
to the next route:
1 2 3 |
public create() { this.router.navigate(['/blog'], { 'queryParams': { 'sid': this.sid } }); } |
This is just like what we saw with the login screen.
The HTML markup that powers the UI will be no more complex than the TypeScript logic that powers it. The HTML can be seen in theĀ src/app/blogs/blogs.component.html like so:
Blogs You Have Written
|
{{ entry.blog.title }}
{{ entry.blog.content }}
Creating a blog entry will have similar logic to what we’ve seen already. Open the project’sĀ src/app/blog/blog.component.ts file and include the following TypeScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import { Component, OnInit } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; @Component({ selector: 'app-blog', templateUrl: './blog.component.html', styleUrls: ['./blog.component.css'] }) export class BlogComponent implements OnInit { private sid: string; public input: any; public constructor(private http: HttpClient, private route: ActivatedRoute, private location: Location) { this.input = { 'title': '', 'content': '' }; } public ngOnInit() { this.route.queryParams.subscribe(params => { this.sid = params['sid']; }); } public save() { if (this.input.title && this.input.content) { let headers = new HttpHeaders({ 'content-type': 'application/json', 'authorization': 'Bearer ' + this.sid }); this.http.post('http://localhost:3000/blog', JSON.stringify(this.input), { headers: headers }) .subscribe(() => this.location.back() ); } } } |
In the above code we are initializing our form data and retrieving the sid
that was passed from the previous page.
When we try to save the entry, we construct an appropriate header that contains the session id and then send it to the API with the form data. Once complete, we can navigate backward in the stack.
The HTML for the UI of this page is in theĀ src/app/blog/blog.component.html file like so:
New Blog Entry
Keeping Consistency in Mind
By default, Couchbase will try to be fast, which means it may return data faster than the indexes update. When this happens you might have freshly created data missing from your results.
I explained how to correct this in a previous tutorial, but a quick recap is below.
1 2 3 4 5 6 7 8 9 10 11 |
const query = `SELECT * FROM `blog` WHERE type = 'blog' AND pid = $PID;` const options = { scanConsistency: couchbase.QueryScanConsistency.RequestPlus, parameters: { PID: request.pid } } await cluster.query(query, options) .then((result) => { console.log(result.rows) response.send(result.rows) }) .catch((e) => response.status(500).send(e)) |
In our blogs
endpoint, we can define the scan consistency, and in this case, we are telling it to wait until the index has updated before returning the data. This is a fast operation, so don’t think that it will crawl when you do this. It just won’t be as fast as the default.
At this point, the front-end should work flawlessly with the backend!
To run both projects together.
First, ensure that you have Couchbase installed and that you have a bucket named blog
created and the index we created from the API tutorial.
Next, run the Node REST API:
1 |
node server |
Then, run the Angular project:
1 |
ng serve |
Register, login, create a blog post or two, and ensure that the blogs route is showing each of the blogs you created.
Ways You Can Improve This Project
As we mentioned before the project could use some custom CSS and layout as well as form validation and alerts or notifications given the user has tried to use the wrong password or blog title exceeds a certain amount of characters or if an error has been thrown because the API is not running properly.
Create a Blog Edit page and route and a link from the title of the blog posts on the Blogs Route/Page.
Create sophisticated navigation and frame for the application.
Re-order the blog list on the Blogs Route/Page or set up the ability to sort blogs by title ascending or descending order.
Allow users to login with social logins or use this project we have created and add a portfolio route and other supporting pages as a starter for your own developer portfolio.
Create an Angular service for handling the session id, so that way it doesn’t need to be passed around with each navigation event.
Conclusion
You just saw how to create a simple client front-end using Angular 11 and TypeScript for the user profile store we saw in a previous example. Every Angular user profile template is different, of course. This client front-end is one of many possible angular material user profile page examples for a front-end because this is a modular full-stack application.Ā This means that the backend can be in any language and the front-end can be in any language.
The finished code is available on GitHub at couchbaselabs / couchbase-angular-blog-app repo on GitHub.
For more information on using Couchbase with Node.js, check out the Couchbase Developer Portal.