# Monorepo API and services
---
## Summary
- Normalizr
- API
- Services
---
## Introduction
The aim of this documentation is to present the essential aspect of normalizr in the context of our application at first and then show how it is used in our API while adressing their general structure and definition. Finally we will see how to create a clean service around it.
---
## Normalizr
---
Normalizr will allow us to do a two things:
- Create processing strategies for each of our entities: assigning default value, flattening our data structure
- lighten our data by cutting its redundancy
---
## Basic usage
---
First you can take a look at the official documentation: https://github.com/paularmstrong/normalizr
---
Since Normalizr is a very extensive library, we will just go over the two basic principles used in myCANAL:
- entities
- preprocessing
---
### Entities
An entity is basically a recurring data structure with specific properties and an identifying field (like an id):
- When you retrieve a list of payment means, each payment mean is an entity
- When you fetch a contentGrid, each content can be seen as an entity
---
### Entity definition
Take this API return:
```javascript
const data = {
returnCode: 'SUCCESS',
data: {
cars:[{
brand: 'peugeot',
model: '306',
immatriculation: 'AX-185-MD',
owner: {
name: 'Mark',
passportNumber: '123456'
},
{
brand: 'renault',
model: 'clio',
immatriculation: 'RS-105-HF',
owner: {
name: 'Mark',
passportNumber: '123456'
}
}]
}
}
```
---
We can identify three entities:
- A car, with its identifying field being its immatriculation
- An owner, with its identifying field being its passport number
- Our API return that will be an entity of its own (further explanation in the preprocessing part)
---
We will define the owner entity first since it is situated the deepest inside our data structure:
```javascript
const owner = new schema.Entity(
'owners',
{},
{ idAttribute: 'passportNumber' }
);
```
- we have first the name of our entity
- second the data structure (it there are any other entity in this entity)
- idAttribute is an option allowing us to inform normalizr of the entity identifying field
---
Then we will define the car entity:
```javascript
const car = new schema.Entity(
'cars',
{ owner: owner },
{ idAttribute: 'immatriculation' }
);
```
As you can see, this time the data structure contains the owner field since it is an entity nested inside the car one
---
And finally our API return
```javascript
const apiSchema = new schema.Entity('result',
{ data:
{
cars: [car]
}
});
```
As you can see, we defined the field cars as an array of cars entity. Besides we didn't define an idAttribute because we know there will be only one return.
---
Besides, it will allow us to add a preprocessing strategy to our API return
---
### Partial normalization
We can then use the normalize function:
```javascript
const normalizedData = normalize(data, apiSchema);
```
---
This will give us the following return:
```javascript
{
entities: {
cars: {
'AX-185-MD': {
brand: 'peugeot',
model: '306',
immatriculation: 'AX-185-MD',
owner: '123456'
},
'RS-105-HF': {
brand: 'renault',
model: 'clio',
immatriculation: 'RS-105-HF'
owner: '123456'
},
},
owners: {
'123456': {
name: 'Mark',
passportNumber: '123456',
}
},
result: { '1': {
returnCode: 'SUCCESS',
data: {
cars: [ 'AX-185-MD', 'RS-105-HF' ],
}
}}
},
result: ['1']
}
```
---
We can see three things:
- Each entity has been stored in its own object, each key is the entity identifying field
- When there are nested entities, the nested entity is replaced with its id
- Without preprocessing, the apiSchema doesn't make much sense
In conclusion, this will allow us to store less data, manipulate smaller javascript object and separate our data into
meaningful entities.
---
## Preprocessing
A preprocessing strategy will allow us to do two things:
- assign default values to our entities optional properties
- flatten our data structure
---
### Definition
We can define our preprocessing strategy for our API as follow:
```javascript
const apiPreprocessStrategy = (api) => ({
returnCode: api.returnCode,
errorMessage: api.errorMessage || '',
cars: api.data.cars,
});
```
---
As you can see we handled those three properties differently:
- returnCode is a mandatory field, we don't need to affect a default value
- errorMessage is not defined when there are no errors, we affect it a falsy string value when not defined
- the field cars is mandatory aswell, but we flattened it cause the data object was useless
---
### Entity revision
Since the preprocessing is done before the normalization process we have to adapt our Entity definition:
```javascript
const apiSchema = new schema.Entity('result',
{
cars: [car]
},
{
processStrategy: apiPreprocessStrategy,
}
);
```
We flatened our data structure aswell and added the preprocess strategy to the option object.
---
You can define a preprocess strategy for each entity and then call normalize like before. The preprocessing strategy of each entity will be then called on each corresponding entity present inside your data structure
---
## API
---
An API in myCANAL context is simply a call to an external API but with added value:
- Error handling
- Typing of the API return
- Logging
---
### API Structure
---
- `ApiNameApi/` Contains the actual API call in a single function
- `ApiNameApi.types/` Contains every API types
- `ApiNameApi/` Export the API call function
- `normalize/ApiNameApi-strategy/` Contains the API preprocess strategies
- `normalize/ApiNameApi-schema/` Contains the API schema and the normalize function
- `normalize/index.ts/` Export the normalize function
---
### API Generic definition
---
*ApiNameApi.types.ts*
This file contains the types of each entity and the api response before and after preprocessing and normalization.
```javascript
/**
* Entity type before preprocessing
*/
export interface IEntityRaw {
// type definition
}
/**
* Entity type after preprocessing
*/
export interface IEntity {
// type definition
}
/**
* Raw answer from the API
*/
export interface IApiNameApiResponseRaw {
// type definition
}
/**
* API answer after preprocessing
*/
export interface IApiNameApiResponsePreprocessed {
// type definition
}
/**
* Normalized api answer
*/
export interface IApiNampeApiResponse {
// type definition
}
```
---
*ApiNameApi-strategy.ts*
This one contains each preprocessing strategy for the entities and the api response
```javascript
export const apiNameApiStrategy = (
api: IApiNameApiAnswerRaw
): IApiNameApiResponsePreprocessed => ({
// Strategy definition
});
export const entityStrategy = (entity: IEntityRaw): IEntity => ({
// Strategy definition
});
```
---
*ApiNameApi-schema.ts*
This contains the Schema of each entity and API response aswell as the normalization function
```javascript
export const entity = new schema.Entity();
export const apiNameApiSchema = new schema.Entity(
'response',
{
entities: [entity],
},
{
idAttribute: 'requestId',
processStrategy: ApiNameApiStrategy,
}
);
// Here we normalize and denormalize just after cause
// most of the time we don't need the entity separation
// but we still keep the preprocessing logic
export default (apiResponse: IApiNameApiResponseRaw): IApiNameApiResponse => {
const normalizedResponse = normalize(apiResponse, apiNameApiSchema);
return denormalize(normalizedResponse.result, apiNameApiSchema, normalizedResponse.entities);
};
```
---
*ApiNameApi.ts*
**TypeGuard**: in typescript you can define typeguard to cast a variable to a certain type with a real test on weither or not the variable meet the requirements to be of this specific type
```javascript
const isValidAPIReturn = (apiReturn: any): apiReturn is IApiNameApiResponseRaw =>
// return false or true if
// the variable meet the requirements
// to be an API return
```
---
```javascript
export default async (parameters): Promise<IApiNameApiResponse> => {
const response = await fetch('url', options);
if (response.status && response.status === 403) {
throw new Error('IApiNameApi::call Error 403 while fetching');
}
const json = await response.json();
if (!isValidAPIReturn(json)) {
throw new Error('IApiNameApi::call Invalid API return');
}
return normalizeApiNameApiReturn({ ...json, requestId: uuid() });
};
```
---
## Service
---
When your API implementation is complete, you need to create a new service that will consume it, propagate the data throughout your application and handle the eventual error.
---
### Basic rules
- A service must have an action that starts it (like a redux action)
- There can only be two outcomes to a service:
- an error
- a success
- each of those outcomes must only have a single way to propagate its result (like a success and error redux action)
- The business rules must be contained in the service and not in the API call (like token renewal).
---
### Service definition
Since we are using redux-saga in mycanal, the service definition will be aswell.
```javascript
export default function* sagaNameSaga(action: ITriggerAction) {
// Retrieving needed value from either state or
// action payload
const requiredValueFromAction = action.payload.value;
const requiredValueFromSelector = yield select(RandomSelectors.requiredValueSelector);
let json;
// API call
try {
json = yield apiToCall(requiredValueFromAction, requiredValueFromSelector);
} catch(e) {
// Error case
yield put(errorAction(e));
}
// Success case
yield put(successAction(json));
}
```
This is the most basic implementation of a service possible you could also:
- Add treatment on the data or filter to the api response
- Have a few business rules
- Have different error handling case depending on the error thrown by the API
{"metaMigratedAt":"2023-06-15T00:04:11.198Z","metaMigratedFrom":"YAML","title":"MyCANAL API and service architecture","breaks":true,"description":"A simple presentation of mycanal API and service architecture","contributors":"[{\"id\":\"1cb13298-4ea0-46e4-bc7f-c19260192f49\",\"add\":14104,\"del\":3750}]"}