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.

  1. NodeJS
  2. Angular CLI
  3. NestJS CLI

 

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.

Angular paginated table

 

Final Github Repository

 

comments powered by Disqus