Communication between Backend and Frontend



The main gateway between our Frontend (Angular) app and Backend (Flask) will controllers that should accept GET and POST with JSON payload HTTP request and return response in JSON format. In common we need to implement REST API for our Frontend. In other case in Frontend we should create special service to manage all requests to this API.

Let's create dummy endpoint to get test list of fake data from Backend.

Create new blueprint controller for our test purposes ./backend/controllers/test.py and add simple method to handle GET request from Angular
from datetime import datetime
from datetime import date
from flask import Blueprint, jsonify, request
from flask_login import login_required
from flask_login import current_user
from models import User
from sqlalchemy.orm import defer  # load_only

test_bp = Blueprint('test', __name__, url_prefix='/test')


@test_bp.route('/', methods=['GET'])
@login_required
def get_test_data():

    arg0 = request.args.get('arg0')

    user2 = User.query.options(defer('password')).get(1)

    arr1 = [{
        'arg0': arg0,
        'arg_bool': True,
        'arg_int': 10,
        'arg_str': 'test me!!!',
        'arg_datetime': datetime.now(),
        'arg_date': date.today(),
        'current_user': current_user.to_dict(defer='password'),
        'user2': user2
    }]

    return jsonify({'arr1': arr1})

Here you can see complex data we want to send to Frontend that contains:
- different simple standard types
- complex types like date and datetime
- data from GET parameters (arg0)
- SQLAlchemy objects that we can serialize different ways.
All these data are placed to the array.

With standard JSON processor we receive this error
TypeError: Object of type date is not JSON serializable

In this case we need to prepare our own JSON serialize method and use it instead of standard from Flask.

./backend/app.py
...
class CustomJSONEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o.__class__, DeclarativeMeta):
            return o.to_dict()
        if isinstance(o, datetime.datetime):
            return str(o)
        if isinstance(o, datetime.date):
            return str(o)
        return super(CustomJSONEncoder, self).default(o)
...

...
app.json_encoder = CustomJSONEncoder
...

First if is designed to SQLAlchemy Models. Also we have custom serialize logic for date and datetime objects. For other types we use standard JSON serializer.

To make SQLAlchemy JSON serializer works too we need to patch models with custom OutputMixin

./backend/models/__init__.py
import json
import datetime
from uuid import UUID
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.declarative import DeclarativeMeta

db = SQLAlchemy()


class OutputMixin(object):
    RELATIONSHIPS_TO_DICT = {}

    def __iter__(self):
        return self.to_dict().iteritems()

    def to_dict(self, rel=None, backref=None, defer=[]):
        if rel is None:
            rel = self.RELATIONSHIPS_TO_DICT
        res = {column.key: getattr(self, attr)
               for attr, column in self.__mapper__.c.items()
               if attr in self.__dict__ and attr not in defer}
        if rel:
            for attr, relation in self.__mapper__.relationships.items():

                # Avoid recursive loop between tables.
                if backref == relation.table or \
                        attr not in self.RELATIONSHIPS_TO_DICT or \
                        attr not in self.__dict__:
                    continue
                value = getattr(self, attr)
                if value is None:
                    res[relation.key] = None
                elif isinstance(value.__class__, DeclarativeMeta):
                    res[relation.key] = value.to_dict(backref=self.__table__)
                else:
                    res[relation.key] = [i.to_dict(backref=self.__table__)
                                         for i in value]
        return res

    def to_json(self, rel=None):
        def extended_encoder(x):
            if isinstance(x, datetime):
                return x.isoformat()
            if isinstance(x, UUID):
                return str(x)
        if rel is None:
            rel = self.RELATIONSHIPS_TO_DICT
        return json.dumps(self.to_dict(rel), default=extended_encoder)


from .user import User  # NOQA

./backend/models/user.py
from models import db, OutputMixin
from flask_login import UserMixin


class User(UserMixin, OutputMixin, db.Model):

    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(255), unique=True, nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)


Frontend:

Now let's create Remote Service to handle all requests to Backend

Create directory services in ./frontend/src/app/
cd ./frontend
ng g service services/remote

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

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

    constructor(
        private http: HttpClient,
        // private spinnerService: SpinnerService
    ) { }

    getTestData(): Promise<any> {
        return this.get('/test/', { arg0: 'Test Variable from Angular' });
    }

    get(url, paramsObj): Promise<any> {
        // this.spinnerService.showSpinner();
        let params = new HttpParams();
        for (const pName of Object.keys(paramsObj)) {
            params = params.set(pName, paramsObj[pName]);
        }
        return this.http.get<any>(url, { params })
            .toPromise()
            .then(result => {
                // this.spinnerService.hideSpinner();
                return result;
            })
            .catch(err => {
                // this.spinnerService.hideSpinner();
                setTimeout(() => {
                    if (err.message) {
                        alert(err.message);
                    } else {
                        console.log(err);
                        alert('Internal Server Error. Please contact administrator.');
                    }
                });
                throw err;
            });
    }

    post(url, payload): Promise<any> {
        // this.spinnerService.showSpinner();
        return this.http.post<any[]>(url, payload)
            .toPromise()
            .then(result => {
                // this.spinnerService.hideSpinner();
                return result;
            })
            .catch(err => {
                // this.spinnerService.hideSpinner();
                setTimeout(() => {
                    if (err.message) {
                        alert(err.message);
                    } else {
                        console.log(err);
                        alert('Internal Server Error. Please contact administrator.');
                    }
                });
                throw err;
            });
    }
}

And finally test for request

./frontend/src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { RemoteService } from './services/remote.service';

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

    constructor(
        private remoteService: RemoteService
    ) {

    }

    ngOnInit() {
        this.remoteService.getTestData()
        .then((data) => {
            console.log(data);
            console.log(data.arr1[0].current_user.username);
        });
    }

}

Here we call method from RemoteService and output the result. As you can see final result is regular JS object created from JSON by Angular and we can access any element with dot notation.

If all is fine you should see this output in your browser









Comments

Popular posts from this blog

HTTPS in local environment for Angular + Flask project.

Salesforce Authentication 2 (Token Validation and Refresh)

User authentification with Flask-Login