Sunday, January 19, 2020

Install Kubernetes on Ubuntu with microk8s

Here is my experience of installing Kubernetes under Ubuntu. In simple terms, most of the time having properly functioning Kubernetes is not an easy task. I tried minikube, it has lots of installation issues, and so far I am more impressed by the microk8s performance. microk8s is available via snap and is small and suitable for local development. Just keep in mind to let at least 5GB of hard drive space for the nodes to work.
(part of the Kubernetes course)

Installation:
sudo snap install microk8s --classic --edge

To start the Kubernetes just use: microk8s start
and to stop it: microk8s stop

Once it loads up you can check the status of the cluster with microk8s.inspect and make changes if prompted to.

Then let's peek inside the cluster:

microk8s.kubectl describe nodes
microk8s.kubectl get all --all-namespaces
If you see that some of the pods/services/deployments are not available you can debug by looking at the logs inside of  /var/log/pods/ directory to find out why.

We have access to addons:
microk8s.status
then we can enable some of them with:
microk8s.enable

You can change the default directory for storage of resources:
by editing the --root and --state values inside: /var/snap/microk8s/current/args/containerd

Now lets authenticate inside the cluster, check the URL from:
microk8s.kubectl get all --all-namespaces
then use username(admin) and password from the output of:
microk8s.config

Let's now see the Kubernetes dashboard, for it to function properly we will enable the following addons:

microk8s.enable dns dashboard metrics-server

In order to browse the dashboard, we will need to know its IP address. Notice the IP and port of the dashboard service:
watch microk8s.kubectl get all --all-namespaces
you might need to browse something like https://10.152.183.1/ (port 443)

For accessing the dashboard URL: get the username and password(token) from microk8s.kubectl config view
or just: microk8s.config
For authentication inside the dashboard, there are several methods, we will use a token:
microk8s.kubectl -n kube-system get secret then find kubernetes-dashboard-token-xxxx
and use:
microk8s.kubectl -n kube-system describe secrets kubernetes-dashboard-token-xxxx
to find the token:
And there you go, you can monitor your Kubernetes in a browser!

You can create and update deployments in a cluster using:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc1/aio/deploy/head.yaml

Last, but not least you can create an alias and use directly kubectl instead of writing all the time microk8s.kubectl with:
sudo snap alias microk8s.kubectl kubectl

If at any point you are not happy with the cluster you can reset it with microk8s.reset


Notes:
Namespaces are used to isolate resources (like certain pod can be in only one namespace) inside a single cluster:
microk8s.kubectl get namespaces
microk8s.kubectl config get-contexts is very useful to find the current cluster name and user name. Context includes a cluster, a user and a namespace and is used to distinguish between multiple clusters we work on. To set a context we use: kubectl config use-context my-context
A node is a worker machine(hardware or virtual). Each node contains the services necessary to run pods and is managed by the master component.
Deployment has inside replica sets, which characterize pods, where the containers reside.

Congratulations and enjoy the course!

Thursday, January 16, 2020

JSON web tokens (JWT) with NodeJS REST API

Here is how to deal with JWT inside REST API routes:
Note: if you want to learn more on JSONWebTokens and REST you can visit this course: Learn Node.js, Express and MongoDB + JWT

 


So let's begin by creating a new directory project:
mkdir auth
setup the project:
npm init -f
Then we will install the following libraries:
for setting up the webserver
npm i express
for the database connection
npm i mongoose
for reading .env files
npm i dotenv
for restarting the nodejs application(webserver)
npm i --save-dev nodemon
for using ES6 syntax inside nodejs
npm i --save-dev @babel/preset-env @babel/core @babel/node
setting up the transpiling inside babel
nano .babelrc
{"presets": ["@babel/preset-env"]}

install eslint
npm install eslint --save-dev
other packages: bcryptjs, jsonwebtoken

to be able to run the code change package.json:
"start": "nodemon --exec babel-node index.js"
start developing from the current directory inside visual studio code:
code .

.env file
DB_CONNECT ="mongodb://127.0.0.1/users"
TOKEN_SECRET = "onetwothreefourfive"

our index.js file:
import express from "express";
import mongoose from "mongoose";
import dotenv from "dotenv";
// import the routes
import routes from "./routes/routes";

// create an express instance
const app = express();

// setup the middleware routes
routes(app);

// config the database credentials
dotenv.config();

// connect to the database
mongoose.connect(
process.env.DB_CONNECT,
{ useNewUrlParser: true, useUnifiedTopology: true },
() => console.log("connected to mongoDB")
);
// listen for errors
mongoose.connection.on('error', console.error.bind(console, 'MongoDB connection error:'));
// listen on port 3000
app.listen(3000, () => console.log("server is running"));


controller.js:
import mongoose from "mongoose";
mongoose.set("useCreateIndex", true);
import { userSchema } from "../models/user.js";
import * as bcrypt from "bcryptjs";
import * as jwt from "jsonwebtoken";

const User = mongoose.model("users", userSchema); // users is the name of our collection!!!
export const addNewUser = (req, res) => {
User.init(() => {
// init() resolves when the indexes have finished building successfully.
// in order for unique check to work
let newUser = new User(req.query); // just creating w/o saving
newUser.password = bcrypt.hashSync(req.query.password, 10); // setting password synchronously
newUser.save((err, user) => { // now saving
if (err) {
res.send(err.message);
}
res.json(user);
});
});
};

export const loginUser = (req, res) => {
User.init(() => {
User.findOne({ email: req.query.email }, (err, user) => {
if (err) {
res.send(err);
}
if (user == null) {
res.status(400).send("Non existing user");
}

// we have the user record from db, now check the password
const validPassword = bcrypt.compareSync(
req.query.password,
user.password
);
if (!validPassword) res.status(400).send("Not valid password");

// create and send a token to be able to use it in further requests
const token = jwt.sign({ _id: user._id }, process.env.TOKEN_SECRET);
res.header("auth-token", token)  // set the token in the header of the response

.send(token); // display the token
});
});
};


routes.js: // our main routes file
import { addNewUser, loginUser } from "../controllers/controller.js";
import { info } from "../controllers/info.js"; // the protected route

import { auth } from "../controllers/verifyToken"; // middleware for validating the token

const routes = app => { 
app.route("/user/register").get((req,res)=>addNewUser(req,res)); // we capture inside req, and res
app.route("/user/login").get((req,res)=>loginUser(req,res)); // we capture inside req, and res
app.route("/info").get(auth,(req,res)=>info(req,res)); // we capture inside req, and res
// and insert the auth middleware to process the token
};
export default routes;


verifytoken.js
import * as jwt from "jsonwebtoken";

export const auth = (req, res, next) => {
const token = req.header("Bearer");
if (!token) return res.status(401).send("access denied");
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
if (!verified) res.status(400).send("Invalid token");
// continue from the middleware to the next processing middleware :)
next();
};

// user mongoDB schema:
user.js
import mongoose from "mongoose";
export const userSchema = new mongoose.Schema(
{
name: { type: String, required: "Enter username", minlength: 5, maxlength: 20 },
email: { type: String, required: "Enter email", maxlength: 50, unique: true },
password: { type: String, required: "Enter password", maxlength: 65 }
},
{
timestamps: true
}
);

Congratulations and enjoy learning !

Thursday, January 09, 2020

Ubuntu server - my list of useful commands

Here is a collection of commands I find very useful when doing daily work on Ubuntu Linux. For more information, you can reference this course.

  1. How to check what is the exact name of a package containing a command with a certain name? With: apt-file we can search inside of contents of a package. 1.1 sudo apt-file update, then 1.2 apt-file search gimp
    or dpkg -S filename
  2. How about checking all the files that have been modified in the last 30 minutes? find -cmin -30 will do the job
  3. or just to see the installed packages: cat /var/log/dpkg.log |grep installed is the command.
  4. Sometimes I need to check what is happening to my disk space. One very good program for the command prompt is ncdu. Just try it, and you won't be disappointed: ncdu
  5. Sometimes I don't know which applications are using the internet or if certain download/update is stalled: nethogs is best for those cases.
  6. And what if I would like to be able to run a certain program without typing constantly sudo in front? Well adding our current user $USER to the program's group is all needed:  sudo usermod -aG docker $USER(here is for running docker without sudo) 
  7. Permissions: you may encounter that there exist additional fine-grained permissions apart from the default for the user/group/and owner. Welcome the ACLs. If you see a + next to the ls -la listing of a file or directory just type getfacl /dir_name/ to see them. To a add a group ACLs: setfacl -m g:www-data:rwx /var/www/, to remove group ACLs: setfacl -x g:www-data /var/www. ACLs also have defaults with -d. This way a new directory/file will inherit the default permissions of the parent directory: setfacl -d -m g:www-data:rwx /var/www Another example: setfacl -Rd -m u:{$USER}:rwx /var/www/ will set rwx permissions to /var/www for a specific user - here we also use the recursive -R flag. Note that sometimes we need to do 2 passes to set permissions correctly: one with defaults -d for any newly created files/dirs, and one without the -d for the current files and directories!
  8. Packages dependency clashes:
    I. We can have different versions of the same package inside of the apt repository for example for bionic and for focal releases. First, check and fix every URL to come from the same distribution inside: /etc/apt/sources.list and the inner directories. Then run apt update again to refresh the updated list of packages and use: dpkg --configure -a to configure the packages.
    II. Interrupted apt dist-upgrade while installing the packages, then later trying again to use apt update && apt dist-upgrade but in between a new version of some package has been released. In this case, you have unmet dependencies because you have an old version about to be installed (staying in the apt-cache list), then a new version comes and it cannot continue with the installation, because the old one is still not installed successfully. Circular dependencies may happen, or when the package version installed on the system is different than the required from another package. For example:
    libqtermwidget5-0:amd64 depends on qtermwidget5-data (= 0.14.1-2); however:
    The version of qtermwidget5-data on the system is 0.14.1-0ubuntu3
    1. remove the half-installed package that causes the problems and its configuration from your system:
    sudo dpkg -P --force-depends
    qtermwidget5-data_0.14.1-2_all 
    2. when we do apt update, the command saves a cached list of packages that will be downloaded from the Ubuntu servers in /var/lib/apt/lists/, so remove all apt caches: sudo find /var/lib/apt/lists -type f  |xargs rm -f >/dev/null and run apt update
    3. configure the rest of the packages to be installed and configured, without checking their dependency requirements (ignore): sudo dpkg --configure -a --force-depends
    4. continue installing the  packages with their correct dependencies: sudo apt-get -f install
    in another version of the problem a new package cannot be installed, because it asks to overwrite a file of an old one: dpkg is trying to overwrite 'file' which is also in package '...'. In this case issue: sudo dpkg -i --force-overwrite /var/cache/apt/archives/libffi8ubuntu1_3.4~20200819gead65ca871-0ubuntu3_amd64.deb (where you can place the name of the new package archive)
    or a more "safe option" is to remove the problem causing archive from /var/cache/apt/archives/
  9. Network tools:
    list all the processes taking most of the CPU: ps -e --format=pid,rss,args | sort --numeric-sort --key=2 check the network connections inside of specific process: sudo lsof -aPi -p 3258 or trace them: sudo strace -p 3258 -e trace=network
    list all listening(open) ports on the current machine: sudo lsof -i | grep -i LISTEN
    list all the network connections of user 'nevyan': sudo lsof -aPi -u nevyan
    listening connections inside of a process:
    sudo lsof -ai -p 730
  10. Working with text files:
    - replacing strings inside of a file:
    sudo sed -i 's/focal/groovy/g' /etc/apt/sources.list
    // s -substitute, g - global, apply to all matches
    - downloading a file and replacing its contents at the same time:
    curl -s https://raw.githubusercontent.com/istio/istio/release-1.6/samples/bookinfo/platform/kube/bookinfo.yaml | sed 's/app: reviews/app: reviews_test/'
    - adding information to text file (-a = append / the default action is overwrite):
    echo "deb https://download.sublimetext.com/ apt/stable/" | sudo tee -a /etc/apt/sources.list.d/sublime-text.list
  11. Working with variables:
    - how to grep webpage tags:
    MYHOST="http://www.google.com";
    curl -s $MYHOST | grep -o "<title>*.*</title>";

    // -o shows only full matched lines with content
    - how to save an output of command:
    look for pattern line and display column 3: ip route | awk '/default/ { print $3 }'
    save output into variable: MYHOST=$(ip route | awk '/default/ { print $3 }')
    ping MYHOST
    - complex example for purging non-used kernels:
    dpkg --list linux-{headers,image,modules}-* | awk '{ if ($1=="ii") print $2 }' | grep -v -e "$(uname -r | cut -d "-" -f 1,2)" | sudo xargs apt purge -y

    // e regex match, v inverted match
    It there are any removed packages, they can be reinstalled with: apt install --reinstall linux-image-X.Y-ARCH5
  12. Finding and deleting files recursively based on pattern: find ./mail -depth -path '*gmai*' -delete
  13. How to do a default prompt shortening, when the shell gets too long: export PS1="\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]\$ "
  14. Ever wanted to be able to resume downloads, here is the curl option: curl -c http://url_to_download
    Congratulations and enjoy the course!

    Tuesday, December 31, 2019

    Angular 8 - Working with reactive forms

    Here is a fully functional component featuring dynamic creation of form elements and filling them with server data. For more information on modern development with Angular you can visit this course.


    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormArray, Validators, FormBuilder } from '@angular/forms';
    let emailRegex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$";


    // initial server data mockup
    let serverData =
    {
    "users": [
    {
    "firstName": "server:John",
    "lastName": "Bernard",
    "email": "john@test.com"
    },
    {
    "firstName": "server:Anna",
    "lastName": "Parker",
    "email": "anna@test.com"
    }
    ],
    "date": 1577788750829
    };

    @Component({
    selector: 'app-root',
    styleUrls: ['./app.component.scss'],
    template: `
    <form [formGroup]="usersForm">
    <!-- digg deeper to get the users controls (they are also controls of usersForm-->
    <!-- usersForm['controls'].users['controls']; -->
    {{ usersForm.get('date').value | date}}
    <ng-container *ngFor="let userFromGroup of usersForm.get('users')['controls']; let i = index" >
    <div class="formgroup" [formGroup]="userFromGroup"> <!-- binds formgroup from the loop -->
    {{i}}
    <!-- get values inside the fg object to bind to controls -->
    <label>First name:<input type="text" formControlName="firstName"></label>
    <label>Last name:<input type="text" formControlName="lastName"></label>
    <label>Email:<input type="text" formControlName="email"></label>
    <button type="button" class="remove" (click)="removeControl(i)">x</button>
    </div>
    </ng-container>
    </form>
    <button type="button" (click)="addNew()">Add new</button>
    <button type="button" (click)="setValues()">Set values</button>
    <button type="button" (click)="patchValues()">Patch values</button>
    <button type="button" (click)="getServerData()">Get from server</button>
    {{usersForm.value|json}}
    `
    })


    export class AppComponent implements OnInit {
    constructor(private fb: FormBuilder) { }

    public usersForm: FormGroup;

    ngOnInit() {
    this.usersForm = this.fb.group({ // get inside the main form group
    users: this.fb.array([ // get inside the array of form control groups
    this.fb.group({ // the controls array[0]
    firstName: ['user 1', { validators: Validators.required }],
    lastName: ['', { validators: Validators.required }],
    email: ['', { validators: Validators.pattern(emailRegex) }]
    }),
    this.fb.group({ // the controls array[1]
    firstName: ['user 2', { validators: Validators.required }],
    lastName: ['', { validators: Validators.required }],
    email: ['', { validators: Validators.pattern(emailRegex) }]
    })
    ]),
    date: Date.now()
    });
    }

    removeControl(i) {
    let uForm = this.usersForm.get('users') as FormArray;
    // we cast to FormArray because of:
    // Property 'removeAt' does not exist on type 'AbstractControl
    uForm.removeAt(1);
    }

    patchValues() {
    let uForm = this.usersForm.get('users') as FormArray;
    uForm.patchValue([
    { email: 'mock@mail.com' }
    ]);
    uForm.controls[0].patchValue(
    { email: 'mock@mail.com' }
    );
    }

    setValues() { // strictly setting data
    let formUsers = this.usersForm.get('users') as FormArray;
    // foreach of the users
    formUsers.setValue([
    {
    firstName: 'John',
    lastName: 'Bernard',
    email: 'john@test.com'
    },
    {
    firstName: 'Anna',
    lastName: 'Parker',
    email: 'anna@test.com'
    }
    ]);
    }


    getServerData() {
    this.usersForm = this.fb.group({
    users: this.fb.array([]),
    // you can also set initial formgroup inside if you like
    /*
    users: this.fb.array(
    serverData.users.map(u =>
    this.fb.group({ // the users from the server
    firstName: [u.firstName, { validators: Validators.required }],
    lastName: [u.lastName, { validators: Validators.required }],
    email: [u.email, { validators: Validators.pattern(emailRegex) }]
    })
    )),
    */
    date: Date.now()
    })

    let usersArray = this.usersForm.get('users') as FormArray;

    serverData.users.map(u => { // for each of the users from the server
    usersArray.push( // push new controls into the usersArray
    this.fb.group({ // set the controls
    firstName: [u.firstName, { validators: Validators.required }],
    lastName: [u.lastName, { validators: Validators.required }],
    email: [u.email, { validators: Validators.pattern(emailRegex) }]
    })
    );
    });
    }

    getInfo() {
    let uForm = this.usersForm.get('users') as FormArray;
    let fcontrol = uForm.controls[0].get('firstName').value;
    console.log(fcontrol);
    }

    addNew() {
    let formsArray = this.usersForm.get('users') as FormArray;
    let arrayLen = formsArray.length;
    let newarrayGroup =
    this.fb.group({ // the controls
    firstName: ['user 3', { validators: Validators.required }],
    lastName: ['', { validators: Validators.required }],
    email: ['', { validators: Validators.pattern(emailRegex) }]
    });
    formsArray.insert(arrayLen, newarrayGroup);
    }
    }

    Congratulations! You can further take a look at more examples in the course!

    Sunday, December 29, 2019

    Star rating script - JavaScript and CSS

    There is a whole course on how to achieve star rating using PHP and JavaScript, and here is a simple way consisting of CSS and JavaScript only:


    <style>
    .rating {
    display: flex;
    padding: 0;
    margin: 0;
    }

    .rating li {
    list-style-type: none
    }

    .rating-item {
    border: 1px solid #fff;
    cursor: pointer;
    font-size:2em;
    color: yellow;
    }

    /* initial: make all stars full */
    .rating-item::before {
    content: "\2605";
    }

    /* make until the clicked star (the rest) empty */
    .rating-item.active ~ .rating-item::before {
    content: "\2606";
    }

    /* on hover make all full */
    .rating:hover .rating-item::before {
    content: "\2605";
    }

    /* make until the hovered (the rest) empty */
    .rating-item:hover ~ .rating-item::before {
    content: "\2606";
    }

    </style>

    &lt!--html markup-->

    <ul class="rating">
    <li class="rating-item" data-rate="1"></li>
    <li class="rating-item active" data-rate="2"></li>
    <li class="rating-item" data-rate="3"></li>
    <li class="rating-item" data-rate="4"></li>
    <li class="rating-item" data-rate="5"></li>
    </ul>


    <script>
    const container = document.querySelector('.rating');
    const items = container.querySelectorAll('.rating-item')
    container.onclick = e => {
    const elClass = e.target.classList;
    // change the rating if the user clicks on a different star
    if (!elClass.contains('active')) {
    items.forEach( // reset the active class on the star
    item => item.classList.remove('active')
    );
    console.log(e.target.getAttribute("data-rate"));
    elClass.add('active'); // add active class to the clicked star
    }
    };
    </script>

    Congratulations !

    Wednesday, December 25, 2019

    Sorted to do list in JavaScript

    JavaScript is a wonderful language as you may discover in the JavaScript for beginners - learn by doing course.

    Here is a short working example on how we can easily sort todos saved inside an array:

    The initial HTML markup:
    <input id="myInput" type="text" /><button id="Add"> Add new LI </button>
    <ul class="todoList"></ul>

    <script>
    // initial array to hold the todos
    const todos = [];

    // we have helper function for doing the actual sorting
    const doSort = (todos) => {
    return todos
    // .map(todo => todo.toLowerCase()) // make all the items lowercase
    .sort((a, b) => {
    // compare 2 words letter by letter
    if (a.value > b.value) { return 1; } // when the first letter is after the second
    if (a.value < b.value) { return -1; } // when the second letter is before the second
    return 0; // if both letters are the same
     }) 
    } // attach event listener to the Add button document.querySelector('#Add').addEventListener('click', () => {

    // get what's inside the input
    const data = document.querySelector('#myInput');

    // push the new todo into the todos array
    todos.push(data.value);

    // create additional helper array with object values and indexes
    var mapped = todos.map(
    (el, i) => ({ index: i, value: el.toLowerCase() })
    );

    // sort the todos
    const sortedTodos = doSort(mapped)
    // restore the originals from the todos array
    .map(el => todos[el.index]);

    // display the sorted totos
    todoList.innerHTML = sortedTodos.map(todo => '<li>' + todo + '</li>').join('');

    // clear the value of the input
    data.value = '';
    });

    //get reference to the todoList
    const todoList = document.querySelector('.todoList');

    </script>

    Congratulations!

    Resources:

    JavaScript for beginners - learn by doing

    Thursday, December 19, 2019

    To do list in Angular

    Here is how to create a simple todo-list in Angular. We will be also using extensively RxJs BehaviorSubject as well as Observables. If you want more information on the techniques used, I advise you to practice with the full Angular for beginners course.




    First install angular with: sudo npm i -g @angular/cli
    Then create a new todo project: ng new todo
    Inside the todo directory create 2 new components and service using the Angular CLI:
    ng g c todos
    ng g c form
    ng g s services/todos

    Next, modify the following files:
    app.component.html
    // to include the new component tags
    <div class="container">
    <h1>{{appTitle}}</h1>
    <app-todos [limit]="3"></app-todos>
    <app-form></app-form>
    </div>
    ...
    app.module.ts
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';

    import { AppComponent } from './app.component';
    import { TodosComponent } from './todos/todos.component';

    // import httpclientmodule to be able to perform http requests
    import { HttpClientModule } from '@angular/common/http';

    // import forms module to be able to use 2-way [(binding)]
    import { FormsModule } from '@angular/forms';
    import { FormComponent } from './form/form.component';

    @NgModule({
    declarations: [
    AppComponent,
    TodosComponent,
    FormComponent
    ],
    // and place it (FormsModule) inside modules to be visible by all derived services and componenets
    imports: [
    BrowserModule,
    HttpClientModule,
    FormsModule //  add into imports the formsmodule to be accessible from the components
    ],
    providers: [],
    bootstrap: [AppComponent]
    })
    export class AppModule { }

    ...
    form.component.html

    <div>
    <input type="text" placeholder="Add todo..."
    <!-- bind the title input field of the form to the model this.title -->
    [(ngModel)]="title"
    <!-- allow with enter to be able to run addTodo() -->
    (keydown.enter)="addTodo()" />
    <button (click)="addTodo()">Add todo</button>
    </div>

    ...
    form.component.ts

    import { Component } from '@angular/core';
    // import both the interface Todo as well as the TodoService
    import { Todo, TodosService } from '../services/todos.service';

    @Component({
    selector: 'app-form',
    templateUrl: './form.component.html',
    styleUrls: ['./form.component.scss']
    })

    export class FormComponent implements OnInit {
    // title will be bound to the title input of the form
    title: string = '';

    // use dependency injection to inject the TodosService
    constructor(private todosService: TodosService) { }

    addTodo() {
    // construct todo object and add it using the service
    const todo: Todo = {
    id: Date.now(),
    title: this.title,
    complete: false,
    }
    this.todosService.addTodo(todo);
    }
    }
    ...


    todos.component.ts
    import { Component, OnInit, Input } from '@angular/core';
    import { TodosService, Todo } from '../services/todos.service';
    import { Observable } from 'rxjs';

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

    constructor(private todosService: TodosService) { }
    private fetchData$: Observable<boolean>;
    private todos$: Observable<Todo[]>;

    // receive the limit from the parent(appcomponent) via input
    @Input() limit: number;

    ngOnInit() {
    // initialize the both observables
    this.fetchData$ = this.todosService.getTodos(this.limit);
    this.todos$ = this.todosService.todos$;
    }

    // call the service
    onChange(id: number) {
    this.todosService.onToggle(id);
    }

    // call the service
    removeTodo(id: number) {
    this.todosService.removeTodo(id);
    }

    }

    ...

    todos.component.html

    <!-- mainly to fetch the http json data & setup this.todos -->
    <div *ngIf="(fetchData$ | async ); else errorFetch"></div>

    <!-- get the observable data and spread it on the page -->
    <ul *ngIf="(todos$ | async ) as todos; else loading">
    <li *ngFor="let todo of todos; let i = index">
    <span [class.done]="todo.complete">
    <input type="checkbox" [checked]="todo.complete" (change)="onChange(todo.id)">
    <!--  bind [checked] to the state complete of todo
     pass todo.id on(change) event,
    and run onChange function from the .ts file
    -->
    {{i + 1}} {{todo.title}}
    </span>
    <small>{{todo.date | date}}</small>
    <!-- use pipe date to format the data: you can see other pipes from API/datepipe on angular website -->
    <button class="remove" (click)="removeTodo(todo.id)">×</button>
    </li>
    </ul>

    <ng-template #loading> Loading, please wait ...</ng-template>
    <ng-template #errorFetch> There is an error while loading the data, please try again ...</ng-template>

    ...

    todos.service.ts

    import { Injectable } from '@angular/core';
    import { Observable, BehaviorSubject, of } from 'rxjs';
    import { tap, map, catchError } from 'rxjs/operators';
    import { HttpClient } from '@angular/common/http';

    // create interface with the structure of a simple todo item
    export interface Todo {
    id: number
    title: string
    complete: boolean
    }

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

    constructor(private http: HttpClient) { }
    public todos: Todo[] = [];
    private subject = new BehaviorSubject<Todo[]>([]); // to be able to retain the last emitted version !
    public todos$ = this.subject.asObservable();

    getTodos(limit: number): Observable<boolean> {

    return this.http.get<Todo[]>(`https://jsonplaceholder.typicode.com/todos?_limit=${limit}`)
    //  just return observable of true or false
    .pipe(
    map((fetchedTodos: Todo[]) => {
    // setup the local this.todos member
    this.todos = fetchedTodos;
    // propagade the todos to subscribers via subject
    this.subject.next(this.todos);
    return true;
    }),
    catchError(err => {
    alert(err.message);
    return of(false);
    })
    );
    }

    onToggle(id: number) {
    // get the todo index by the provided ID
    const idx = this.todos.findIndex(todo => todo.id === id);
    this.todos[idx].complete = !this.todos[idx].complete;
    }

    removeTodo(id: number) {
    this.todos = this.todos.filter(todo => { return todo.id !== id }
    );

    // propagade the updated data back to all observables
    this.subject.next(this.todos);
    }

    addTodo(todo: Todo) {
    this.todos = [...this.todos, todo];
    this.subject.next(this.todos);
    }
    }

    Now you can run: ng serve
    and browse: http://127.0.0.1:4200

    Congratulations and enjoy learning Angular.

    Subscribe To My Channel for updates