Crud Api w/ Server Side Pagination with NestJS & Angular - Angular Generic Service
Published on
In a previous post we created generic crud service that we could extend from in order to provide basic crud operations to an Angluar service. I had received a comment and was ask how I would apply this pattern with pagination. Pagination can be either client side or server side so this is really dependant on how the pagination is implemented.
Since we just created server side pagination, I figured I’d provide an update to the generic crud service and modify it to work with a paginated findAll
method.
Prerquisites
The following dependencies are required for this project.
Setup
This post builds upon our previous post here. You can also clone this Github repository.
Adding Bootstrap
We are going to install bootstrap for our example project to provide general styles.
npm install --save bootstrap
And we will import our bootstrap css in our root styles.scss
file…
// src/styles.scss
@import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
Creating Model Classes For Pagination
Before we create our model class, we’re going to generate a core module for our application. From the root of the client
directory run…
ng generate module core
Then update our AppModule
with the new CoreModule
…
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { CoreModule } from './core/core.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
CoreModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Our model classes will mirror what we did on the server side. We’ll put our model class in our core module.
mkdir -p src/app/core/models
touch src/app/core/models/page-request.model.ts \
src/app/core/models/page.model.ts \
src/app/core/models/sort-direction.enum.ts \
src/app/core/models/sort.model.ts \
src/app/core/models/user.model.ts
Our page request model…
import { Sort } from './sort.model';
export class PageRequest {
public page: number;
public size: number;
public sort: Sort;
constructor(page: number = 1, size: number = 10, sort: Sort = new Sort()) {
this.page = page;
this.size = size;
this.sort = sort;
}
public static from(page: number, size: number, sortColumn: string, sortDirection: string): PageRequest {
const sort: Sort = Sort.from(sortColumn, sortDirection);
return new PageRequest(page, size, sort);
}
}
Our page model…
import { PageRequest } from './page-request.model';
export class Page<T> {
public elements: T[];
public totalElements: number;
public totalPages: number;
public current: PageRequest;
public next: PageRequest;
public previous: PageRequest;
constructor(obj: any) {
Object.assign(this, obj);
}
}
Our sort direction enum…
export enum SortDirection {
ASCENDING = 'ASC',
DESCENDING = 'DESC'
}
Our sort class…
import { SortDirection } from './sort-direction.enum';
export class Sort {
public direction: SortDirection;
public column: string;
constructor(column: string = 'id', direction: SortDirection = SortDirection.ASCENDING) {
this.direction = direction;
this.column = column;
}
public static from(column: string, direction: string): Sort {
switch (direction.toUpperCase()) {
case 'ASC': return new Sort(column, SortDirection.ASCENDING);
case 'DESC': return new Sort(column, SortDirection.DESCENDING);
default: return new Sort(column, SortDirection.ASCENDING);
}
}
}
And finally our user model…
export class User {
public id: number;
public firstName: string;
public lastName: string;
public email: string;
constructor(obj: any) {
Object.assign(this, obj);
}
}
These classes are for the most part identical to the classes we created server side so you could copy those over to your client projects. On the client models I did strip out some functionality that wasn’t need on the client side.
Creating Our Generic Crud Service W/ Pagination Support
Our generic crud service is a take on our previous attempt at this which can be found here. First lets create our files…
mkdir -p src/core/services
touch src/app/core/services/crud-operations.interface.ts \
src/app/core/services/abstract-crud.service.ts
Our crud operations interface…
import { Observable } from 'rxjs';
import { Page } from '../models/page.model';
import { PageRequest } from '../models/page-request.model';
export interface CrudOperations<T, ID> {
save(t: T): Observable<T>;
update(id: ID, t: T): Observable<T>;
findOne(id: ID): Observable<T>;
findAll(pageRequest?: PageRequest): Observable<Page<T>>;
delete(id: ID): Observable<any>;
}
You will notice a slight change from the first version of this interface. The findAll
method now takes in a optional PageRequest
as an argument and returns an Observable<Page<T>>
. The PageReqeust
is optional since defaults values will be assigned server side if theses values are omitted from the query string.
Our abstract crud service…
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { CrudOperations } from './crud-operations.interface';
import { Page } from '../models/page.model';
import { PageRequest } from '../models/page-request.model';
export abstract class AbstractCrudService<T, ID> implements CrudOperations<T, ID> {
constructor(
protected _http: HttpClient,
protected _base: string
) {}
save(t: T): Observable<T> {
return this._http.post<T>(this._base, t);
}
update(id: ID, t: T): Observable<T> {
return this._http.put<T>(this._base + "/" + id, t, {});
}
findOne(id: ID): Observable<T> {
return this._http.get<T>(this._base + "/" + id);
}
findAll(pageRequest?: PageRequest): Observable<Page<T>> {
const params: {[key: string]: any} = !pageRequest ? {} : {
pageNumber: pageRequest.page,
pageSize: pageRequest.size,
sortCol: pageRequest.sort.column,
sortDir: pageRequest.sort.direction
};
return this._http.get<Page<T>>(this._base, { params: params });
}
delete(id: ID): Observable<T> {
return this._http.delete<T>(this._base + '/' + id);
}
}
Our findAll
method simply creates query params if pageRequest
is not null
, otherwise an empty object is used for the params (the default values will be assigned on the server side);
Now we need to generate our users service which will extend our AbstractCrudService
. We will also need to add our base url to our environment.ts file
Lets generate our users service…
ng generate service core/services/users
And our users service implementation…
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AbstractCrudService } from './abstract-crud.service';
import { User } from '../models/user.model';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class UsersService extends AbstractCrudService<User, number> {
constructor(protected _http: HttpClient) {
super(_http, `${environment.api.baseUrl}/users`)
}
}
Now we can update our environments.ts file with our base url…
export const environment = {
production: false,
api: {
baseUrl: 'http://localhost:3000'
}
};
And update our core module (we need to import HttpClientModule
to inject the HttpClient
into our service)…
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [],
imports: [
CommonModule,
HttpClientModule
]
})
export class CoreModule { }
Creating A Paginated Users Table
Now that our services are setup, we can create a component that displays our pages in a table. We’ll create buttons for the next page and the previous pages. Our app component template will look something like this.
<div class="container">
<div class="row">
<div class="col-sm-12">
<h2 class="my-4">Users</h2>
<table class="table table-striped">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody *ngIf="currentPage">
<tr *ngFor="let user of currentPage?.elements">
<td>{{ user.firstName }}</td>
<td>{{ user.lastName }}</td>
<td>{{ user.email }}</td>
</tr>
</tbody>
</table>
<div>
<button
class="btn btn-primary mr-2"
role="button"
type="button"
[disabled]="currentPage?.previous?.page > currentPage?.current?.page"
(click)="prevPage()">
Previous
</button>
<span>{{ currentPage?.current?.page }} of {{ currentPage?.totalPages }}</span>
<button
class="btn btn-primary ml-2"
role="button"
type="button"
[disabled]="currentPage?.next?.page < currentPage?.current?.page"
(click)="nextPage()">
Next
</button>
</div>
</div>
</div>
</div>
And our app component code behind file…
import { Component, OnInit } from '@angular/core';
import { take } from 'rxjs/operators';
import { User } from './core/models/user.model';
import { Page } from './core/models/page.model';
import { UsersService } from './core/services/users.service';
import { PageRequest } from './core/models/page-request.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public currentPage: Page<User>;
constructor(
private _usersService: UsersService
) {}
ngOnInit(): void {
this._fetchPageOfUsers();
}
public nextPage(): void {
this._fetchPageOfUsers(this.currentPage.next);
}
public prevPage(): void {
this._fetchPageOfUsers(this.currentPage.previous);
}
private _fetchPageOfUsers(pageRequest?: PageRequest): void {
this._usersService.findAll(pageRequest)
.pipe(take(1))
.subscribe(page => this.currentPage = page);
}
}
We can now open up two terminal windows and start both the client and the server projects together. If we navigate to http://localhost:4200
we should see our paginated table.
Final Github Repository