DEV Community

loading...

ASP.NET Core, Carter and Angular, a scalable RESTfull example for ¿beginners? (2)

sebalr profile image Sebastian Larrieu ・9 min read

Hi everyone again. If you are reading this maybe you finished my previous tutorial:

Or just want a nice example of how to write a scalabe Angular app. Remember, we are building and over engineered to-cook app (like to-do but with food) starting with some basic structure an refactoring later to make it scalable.

Here is the Github repo for this tutorial.

Let me remains you that I'm not a native english speaker so you could find some errors, please let me know and i will correct them

Let me start with some basics ideas we like to follow in my company after various Angular projects.

  1. Core folder with basic layout components, navbar, sidebar and footer.
  2. Shared backend services to communicate with APIs.
  3. Modules (not only components) to encapsulate functionality I.E RecipeModule with CRUD components for recipes.
  4. Classes (DTOs) to share data between backend and frontend (instead of interfaces).
  5. ViewModels that encapsulate DTOs to use in .ts and .html.
  6. Async-Await instead to make code look better.
  7. Global exception handler to show backend error messages (instead of catching in every http request).

In this tutorial we will build the starter Angular template with some (but not all, as this is a serie) of those concepts using CLI.

Lets begin with a new Angular project. First install CLI if you don't have it, then create a new app and select yes when it ask for routing module and sass for stylesheet.

npm install -g @angular/cli
ng new cook-app
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? 
  CSS 
❯ Sass   [ http://sass-lang.com   ] 
  Less   [ http://lesscss.org     ] 
  Stylus [ http://stylus-lang.com ] 

Now we are going to install bootstrap and crate some folders:

cd cook-app
npm install bootstrap
cd src/app
mkdir shared
mkdir dtos
cd shared
touch constants.ts
mkdir backend-services

In constants we will hold the API url for prod and dev enviroment so open file and paste:

import { environment } from 'src/environments/environment.prod';

export const apiUrl = environment.production ? 'http://localhost:5000' : 'http://localhost:5000';

Lets build te comunicaction with the API. Go to dtos folder and create a recipe.dto.ts, it will have the same properties as the backend model and a static method to generate it from a json

export class RecipeDTO {

  id: number;
  title: string;
  description: string;

  public constructor() { }

  public static prepareFromJson(json: any): RecipeDTO {
    const recipe = new RecipeDTO();

    recipe.id = json.id ? json.id : 0;
    recipe.title = json.title ? json.title : undefined;
    recipe.description = json.description ? json.description : undefined;

    return recipe;
  }

}

Now use CLI to create a service in backend-services folder.

ng g service recipeBackend

In this service we will write the method to comunicate with backend, lets start with getting all the recipes:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { apiUrl } from 'src/app/shared/constants';
import { RecipeDTO } from '../dtos/recipe.dto';

@Injectable({
  providedIn: 'root'
})
export class RecipeBackendService {

  constructor(private httpClient: HttpClient) { }

  public async getAll(): Promise<Array<RecipeDTO>> {
    const recipes = new Array<RecipeDTO>();

    const res = await this.httpClient.get<any>(`${apiUrl}/recipe`).toPromise();

    for (const item of res) {
      recipes.push(RecipeDTO.prepareFromJson(item));
    }
    return recipes;
  }

}

As i tell you before, i use await because code look cleaner that .then() chains. You may be wondering what about error as there is no try-catch. Well, for now we will asume that everything is going to work (sadly this never happen in real life), in next post we will implement an exception handler that will handle all errors in one place.

In Angular, services are singleton, we provide this service in 'root' because we want it to be available for all who need access to the API. As you can see, we have Dependency Injections in similar way we had in ASP.NET Core, here we receive a HttpClient that can make a http request to an url and we do not have to worry about how it works, or even how to create it!... This is why I love inversion of control.

Lets continue with get one recipe base on its id:

  public async getById(id: number): Promise<RecipeDTO> {
    const res = await this.httpClient.get<any>(`${apiUrl}/recipe/${id}`).toPromise();
    return RecipeDTO.prepareFromJson(res);
  }

Again, method is async because it await for the http request before continue. As you can see, we don't return a RecipeDTO but a Promise, a Promise is Typescript (or javascript) way to tell that if you call getByid, you could get a result some time in the future. That is why we have to await for the http request, we force our asynchronous code to look like a synchronous one (executing line after line).

Lets make some changes so we can see results before continue with POST, PUT and DELETE.

We are going to create the recipes module for CRUD operations. Go to src/app folder and run:

ng g module recipe
ng g component recipe
ng g service recipe
touch recipe-routing.module.ts

What have we done?

  1. We created a new module. Yes, we can work with plain components but this is a good way of keeping our application modular.
  2. We created the recipe component (.ts .html and .scss)
  3. We created a recipe service, like services in backend, it will help us resolve complicated thing related to recipes.
  4. We created a routing module, every angular module need a routing module to define its routes. (We can write all routes in app-routing.module, but that is not SOLID).

Angular CLI does some work for us, for example if you open recipe.module.ts you will see that RecipeComponent it's alredy imported.

Open recipe-routing.module.ts and write:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { RecipeComponent } from './recipe.component';

const routes: Routes = [{
  path: '', component: RecipeComponent,
}
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class RecipeRoutingModule { }

We are telling Angular routing that if the route is empty, it should load the RecipeComponent. Now we need to import this RoutingModule in RecipeModule, so open recipe.module.ts and write RecipeRoutingModule in imports array (don't forget to import it).

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RecipeRoutingModule } from './recipe-routing.module';
import { RecipeComponent } from './recipe.component';

@NgModule({
  declarations: [
    RecipeComponent
  ],
  imports: [
    CommonModule,
    RecipeRoutingModule,
  ]
})
export class RecipeModule { }

We don't want our components to interact directly with the backend services, we need an abstraction layer (think like this is database access layer and we need providers), there are many reasons for this, maybe we could replace the providers for a stub implementation if we are testing. Maybe backend its not ready and we are going to use in memory info.

This is why we created a RecipeService before. This service does not have too many lines but is importat, so let me show it before explain

import { Injectable } from '@angular/core';
import { RecipeBackendService } from '../shared/backend-services/recipe-backend.service';
import { Subject } from 'rxjs';
import { RecipeDTO } from '../shared/dtos/recipe.dto';

@Injectable({
  providedIn: 'root'
})
export class RecipeService {

  private recipes = new Subject<Array<RecipeDTO>>();

  get Recipes() { return this.recipes.asObservable(); }

  constructor(private recipeBackend: RecipeBackendService) { }

  public async getAllRecipes(): Promise<void> {
    const res = await this.recipeBackend.getAll();
    if (res) {
      this.recipes.next(res);
    }
  }

  public async getRecipe(id: number): Promise<RecipeDTO> {
    const res = await this.recipeBackend.getById(id);
    return res;
  }
}

First we have a recipes Subject.

Big parentheses open Subject are like an evolved Promise, a promise only execute once (with a .then() or await) and give a result but a Subject is more like a flow, it could emit values and anyone who know the subject could receive those values. This is a very basic and poor definition but the ¿beginners? in title is because I asume some basic knowledge, there are many greats articles in this community about promises, async, observables, etc. Big parentheses close

Then we have a getter of the subject as an observable, that is because we don't want anyone outside this service to change the recipes array (only receive).

We need to communicate with backed, so we receive an RecipeBackendService, do you notice how we simple don't care how this service work?. If you remember, the backend service need a httpClient, without IoC we would have had to do something like:

const httpClient = new HttpClient(...);
const serviceBackend = new RecipeBackendService(httpClient);

Before using the serviceBackend, moreover, who knows what that HttpClient constructor need... You have to admit that IoC is a great tool.

Finally we have two methods. getById only work as a middle ware, but getAllRecipes it's a little more interesting. First it get all the recipes from the backend service and after that it pass the array to the subject (that's what next does) so everyone subscribed to the Recipes observable will get the new array.

Now that we have our provider, we could show some data. First lets import bootstrap, there are many things to do this, but I like this: go to to styles.scss in root project folder (the root scss file in app) and paste

@import '~bootstrap/dist/css/bootstrap.min.css';

We use httpClient but we don't tell Angular, so we have to do that. Like this is a global service, we only have to declare it once, so open app.module and add HttpClientModule and RouterModule to imports.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    RouterModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We had to import RouterModule because we are going to do some routing in app component. Open app.component.html and replace the content with

<header>
  <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
    <a class="navbar-brand" href="#">Recipes list</a>
  </nav>
</header>
<main role="main" class="container">
  <router-outlet></router-outlet>
</main>

We just defined a basic common structure with a navbar and a main container (in future tutorials we will refactor this). In main container we put a , here Angular will load modules defined as AppModule' child. Every module could have one (in a mamushka style) where it could load its own childs.

Now we have to modify app-routing.module.ts to indicate that Recipes is a child route.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: 'src/app/recipe/recipe.module#RecipeModule' }
];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

This is the root module so we import routes with .forRoot (because Mr Angular want it this way). We are telling that if route is empty, angular should load the RecipeModule. This is a lazy import, meaning that the module is not loaded until is required by a route navigation, this is very cool for performance, some projects could have hundreds of modules, imagine if you had to wait for all the screens to be loaded and you only want to navigate to your profile module (I'm talking to you php).

We are very close to see some results. Now open recipe.component.ts and write

import { Observable } from 'rxjs';
import { RecipeService } from './recipe.service';
import { RecipeDTO } from '../shared/dtos/recipe.dto';

@Component({
  selector: 'app-recipe',
  templateUrl: './recipe.component.html',
  styleUrls: ['./recipe.component.sass']
})
export class RecipeComponent implements OnInit {

  public recipes$: Observable<Array<RecipeDTO>>;

  constructor(private recipeService: RecipeService) {
    this.recipes$ = this.recipeService.Recipes;
  }

  ngOnInit() {
    this.recipeService.getAllRecipes();
  }

}

Okey, whats going on?. First we declare a public Observable, remember observable? if you don't, is like flow of data we can suscribe to receive information. Recipes is public because we are going to use it in html and typescript compiler will complain if we use a property in html that is not public in the component (if we compile in production mode).

Then in the constructor we get the recipe observable that we declare in the RecipeService (thanks to DI, we have no idea of how this work, we just got it). After, we call the getAllRecipes to update the data flow.

The last step is show the data. Modify recipe.component.html with:

<ng-container *ngIf="recipes$ | async as recipes; else loadingTemplate">
  <ng-container *ngIf="recipes.length > 0; else noDataTemplate">
    <ul class="list-group">
      <li class="list-group-item" *ngFor="let recipe of recipes">
        {{recipe.title}}
      </li>
    </ul>
  </ng-container>
  <ng-template #noDataTemplate>No data.</ng-template>
</ng-container>
<ng-template #loadingTemplate>Loading...</ng-template>

Here many things are happening. First we use ng-container to check if the recipe exist and if it has data. The great thing about ng-container is that it don't create any html tag. We could have used div, but not everything that works is correct.

The ng-templates are like html variable, so if there are some recipes, we show them but if there aren't, we show a no data message.

Finally we are going to see some meals!, first go to the backend project root folder and run:

dotnet run

Now run the Angular project, in root folder:

npm start

Go to localhost:4200 and TADA!:

Okey, lets call it a day. In order to make the POST and PUT we should create view models, that means refactor, that means next post in this tutorial series.

Discussion (0)

pic
Editor guide