|
| 1 | +# 02 Create API and expose it in Container |
| 2 | + |
| 3 | +In this sample we are going to add an Api to our application and integrate it into our Container component, replacing the previously added hardcoded data. |
| 4 | + |
| 5 | +We will use as start up point _01 Hardcoded list component. |
| 6 | + |
| 7 | +Summary steps: |
| 8 | + |
| 9 | +- Create a data model for the API. |
| 10 | +- Implement an API call that fetches data from the web and parses it into the previously defined data model. |
| 11 | +- Create an auxiliary mapper module to parse from the api data model to the view model used in the Container component tree. |
| 12 | +- Modify the container component to use the API calls and the subsequently returned data. |
| 13 | + |
| 14 | +# Prerequisites |
| 15 | + |
| 16 | +Install [Node.js and npm](https://nodejs.org/en/) if they are not already installed on your computer. |
| 17 | + |
| 18 | +> Verify that you are running at least node v6.x.x and npm 5.x.x by running `node -v` and `npm -v` in a terminal/console window. Older versions may produce errors. |
| 19 | +
|
| 20 | +## Steps to build it |
| 21 | + |
| 22 | +- Copy the content of the `01 Hardcoded list component` folder to an empty folder for the sample. |
| 23 | + |
| 24 | +- Install the npm packages described in the `package.json` and verify that it works: |
| 25 | + |
| 26 | +``` |
| 27 | +npm install |
| 28 | +``` |
| 29 | + |
| 30 | +- We will start by creating a suitable folder structure for our new API. |
| 31 | +``` |
| 32 | +└── api/ |
| 33 | + └── model/ |
| 34 | + ├── index.ts |
| 35 | + ├── member.ts |
| 36 | + ├── memberApi.ts |
| 37 | + └── index.ts |
| 38 | +``` |
| 39 | + |
| 40 | +- First, we add an `api subfolder` inside `src`. Then, inside `src/api` we create a new subfolder named `src/api/model` |
| 41 | + |
| 42 | +- Let's start by defining the data model used in our API. Our data source will be a list of GitHub members. As we know from the previous case, we want to be able to display the id, name and profile image (represented as an Url) inside our members page. Thus, the data that we really care to retrieve should hold 1 number property for the id, and 2 string properties for the user login name and avatar image Url respectively . Consequently, we will create a `member.ts` file inside `src/api/model` with the content below: |
| 43 | + |
| 44 | +```javascript |
| 45 | +export interface MemberEntity { |
| 46 | + login: string; |
| 47 | + id: number; |
| 48 | + avatar_url: string; |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +- And since we want to be able to access this interface later from both the API call itself (to properly format the data fetched) and from our auxiliary mapper modulee (to ensure that we can parse from this interface to the one used internally in our view model), we will also define a barrel `index.ts` file for our `src/api/model` folder, as follows: |
| 53 | + |
| 54 | +´´´javascript |
| 55 | +export {MemberEntity} from './member'; |
| 56 | +´´´ |
| 57 | + |
| 58 | +- Next we can start working on our `memberApi.ts` file. We will import our data model from `./model` barrel index file. We will also need to define some constants to store the root Url for our data source service, and the specific endpoint we want to call to retrieve the list of members. We can do by adding the following lines. |
| 59 | + |
| 60 | +```javascript |
| 61 | +import { MemberEntity } from './model'; |
| 62 | + |
| 63 | +const baseRoot = 'https://api.github.com/orgs/lemoncode'; |
| 64 | +const membersURL = `${baseRoot}/members` |
| 65 | +``` |
| 66 | + |
| 67 | +- We want to define a get/fetch REST call to retrieve our list of members from the server. In order to do this, we must send an aynchronous call to the server, using a Promise to store said data once it is available in our app. Thus, we define a `fetchMemberList` method that performs the aformentioned 'fetch' operation and parses the corresponding data, as follows: |
| 68 | + |
| 69 | +```javascript |
| 70 | +export const fetchMemberList = () : Promise<MemberEntity[]> => { |
| 71 | + |
| 72 | + return fetch(membersURL) |
| 73 | + .then(checkStatus) |
| 74 | + .then(parseJSON) |
| 75 | + .then(resolveMembers) |
| 76 | + |
| 77 | + |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +- As noted in the code above, we will first fetch the results from our Url endpoint, and then we will first check that the data could be retrieved successfully, parse said data into JSON, and finally resolve said data according to the API data model we have defined. |
| 82 | + |
| 83 | +- Regarding thee `checkStatus` method, we will simply forward the response if we got an OK reply from the Backend. Otherwise, we will throw an error according to the status received. Notice that we do not need to wrap the returned value inside a Promise (for example, using `Promise.resolve()`), as the `then` call already returns a promise resolved with the data returned. Thus, we can chain then properly without incurring any typing errors on behalf of Typescript. |
| 84 | + |
| 85 | +```javascript |
| 86 | +const checkStatus = (response : Response) : Response => { |
| 87 | + if (response.status >= 200 && response.status < 300) { |
| 88 | + return response; |
| 89 | + } else { |
| 90 | + let error = new Error(response.statusText); |
| 91 | + throw error; |
| 92 | + } |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +- If the members data was retrieved succesfully, we then take the corresponding JSON content. |
| 97 | + |
| 98 | +```javascript |
| 99 | +const parseJSON = (response : Response) : any => { |
| 100 | + return response.json(); |
| 101 | +} |
| 102 | + |
| 103 | +``` |
| 104 | + |
| 105 | +- And finally, for each object in our data list (i.e. for each member in our members list), we will retrieve the three values we are interested in (using destructuring to make the code more concise), build a new object with these 3 values (using the short syntax for property assignment, i.e. `{id, login, avatar_url} equals {id:id, login:login, avatar_url:avatar_url})`), and finally we 'cast' our object into our api data model, as we do meet the required interface (types match). |
| 106 | + |
| 107 | +```javascript |
| 108 | +const resolveMembers = (data : any) : MemberEntity[] => { |
| 109 | + const members = data.map( |
| 110 | + ({id, login, avatar_url,}) => ({ id, login, avatar_url, } as MemberEntity) |
| 111 | + ); |
| 112 | + |
| 113 | + return members; |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | + |
| 118 | +- We have finished our API, now we need to do some changes on our container file and folder to properly expose the API to it. |
| 119 | + |
| 120 | +- First, we will create a new file inside our `src/pages/members` folder called `mapper.ts`. This will be an auxiliary file that parses between our api data model and the view model used in our components. The code we need to add would be the following: |
| 121 | + |
| 122 | +```javascript |
| 123 | +import * as apiModel from '../../api/model'; |
| 124 | +import * as vm from './viewModel'; |
| 125 | + |
| 126 | +const mapMemberFromModelToVm = (member: apiModel.MemberEntity) : vm.MemberEntity => ( |
| 127 | + { |
| 128 | + id: member.id, |
| 129 | + avatarUrl: member.avatar_url, |
| 130 | + name: member.login, |
| 131 | + } |
| 132 | +) |
| 133 | + |
| 134 | +export const mapMemberListFromModelToVm = (memberList: apiModel.MemberEntity[]) : vm.MemberEntity[] => ( |
| 135 | + memberList.map(mapMemberFromModelToVm) |
| 136 | +) |
| 137 | +``` |
| 138 | + |
| 139 | +- The method `mapMemberListFromModelToVm` is the one that actually maps the members list retrieved from the Backend into the data model used in our components. Internally, it will call `mapMemberFromModelToVm` to process and parse each member object inside the list. We do not need to use this parsing method outside of our container, so we will not be adding any methods from `mapper.ts` into the `index.ts` file of our container folder. |
| 140 | + |
| 141 | +- The last steps remaining will revolve around changing the code of our `container.tsx` component to use the new API endpoint alongside our mapper's parsing method. First, we will start by adding the new dependencies to our file header. |
| 142 | + |
| 143 | +```diff |
| 144 | + import * as React from 'react'; |
| 145 | + import { MemberListPage } from './page'; |
| 146 | + import { MemberEntity } from './viewModel'; |
| 147 | ++ import { fetchMemberList } from '../../api'; |
| 148 | ++ import { mapMemberListFromModelToVm } from './mapper'; |
| 149 | +``` |
| 150 | + |
| 151 | +- And finally, we will replace the hardcoded block of the `fetchMembers` method to use instead a call to our `fetchMemberList` API endpoint |
| 152 | + |
| 153 | +```diff |
| 154 | + export class MemberListContainer extends React.Component<{}, State> { |
| 155 | + |
| 156 | + constructor(props) { |
| 157 | + super(props); |
| 158 | + this.state = { memberList: [] }; |
| 159 | + } |
| 160 | + |
| 161 | + fetchMembers = () => { |
| 162 | +- setTimeout(() => { |
| 163 | +- this.setState({ |
| 164 | +- memberList: [ |
| 165 | +- { |
| 166 | +- id: 1, |
| 167 | +- name: 'John', |
| 168 | +- avatarUrl: 'https://avatars1.githubusercontent.com/u/1457912?v=4', |
| 169 | +- }, |
| 170 | +- { |
| 171 | +- id: 2, |
| 172 | +- name: 'Martin', |
| 173 | +- avatarUrl: 'https://avatars2.githubusercontent.com/u/4374977?v=4', |
| 174 | +- }, |
| 175 | +- ] |
| 176 | +- }); |
| 177 | +- }, 500); |
| 178 | ++ fetchMemberList().then((memberList) => { |
| 179 | ++ this.setState({ |
| 180 | ++ memberList: mapMemberListFromModelToVm(memberList), |
| 181 | ++ }); |
| 182 | ++ }); |
| 183 | + } |
| 184 | + |
| 185 | +``` |
| 186 | + |
| 187 | +Now if you execute `npm start` and go to `http://localhost:8080/`, you will see the list of members retrieved from our Url. |
| 188 | + |
| 189 | +# About Lemoncode |
| 190 | + |
| 191 | +We are a team of long-term experienced freelance developers, established as a group in 2010. |
| 192 | +We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us. |
0 commit comments