As of now, you have completed Project Week 4 and now have a backend implementation where your todo lists are saved to a file. The next step is to create backend services that will return todo lists we have saved to file. If you run the solution from week 4 you'll notice that everytime you create new todo lists, the file they are stored in is updated, but your aren't able to see the currently existing todo lists stored. By adding these new backend services this week we'll be able to see the stored todo list entries and allow users to query for specific todo lists. In this week's assignmment you'll create two backend services 1) return all todo lists, 2) return all todo lists with a specified name.
Feature requirements (Week5 task is complete when you):
- Create a backend service to handle a GET request to return all todo lists
- Create a backend service to handle a GET request to return todo lists that match the name sent as a parameter to this request Optional
- From the front-end call the back-end service to get all todo lists currently stored when a user opens the Home page
- Create UI components (a textbox and a button) in the front-end to facilitate searching
Implementation requirements:
- Continue using the front-end and back-end frameworks from week 4.
- Open to-do-list/backend/server.js
- Go to the GET listener "app.get("/get/items", getItems)"
- At the comment "//begin here" copy/paste/type the following code to read in the todo lists stored in the database.json file:
var data = await fsPromises.readFile("database.json");
- Return a response to whoever called the data we just read in, we will return the data from the file but parsed as JSON data:
response.json(JSON.parse(data));
We will test this service using the curl utility. The curl utility is quite useful because it can send requests to services, simulating consuming applications that would utilize the backend service.
- Stop the backend service if it's currently running (Ctrl-C the terminal/command window where you did the last "npm start" for the backend)
- Start the backend service, go to the "to-do-list/backend" directory and install required packages and start the backend:
npm install
npm start
- Open another terminal or command window. Type this curl command to send a request to this service:
curl http://localhost:8080/get/items
- Open to-do-list/backend/server.js
- Go to the GET listener "app.get("/get/searchitem",searchItems)"
- On the line after the comment "//begin here" copy/paste/type the following code, this will retrieve a parameter passed to this service, this parameter will be the name of the Todo List we will search for:
var searchField = request.query.taskname;
- Continue editing this function by adding the following to read in the database
var json = JSON.parse (await fsPromises.readFile("database.json"));
- Add the following to take the data from the database and apply a filter, this will seperate out only the Todo lists that match our search parameter given to the backend service and stored in "searchField":
var returnData = json.filter(jsondata => jsondata.Task === searchField);
- Whether we have data to return (i.e. todo lists that matches the name we're looking for) or not (i.e. there were no todo lists with the name), we return a response to whoever called this service with the following:
response.json(returnData);
-
Stop the backend service if it's currently running (Ctrl-C the terminal/command window where you did the last "npm start" for the backend)
-
Start the backend service, go to the "to-do-list/backend" directory and install required packages and start the backend (you can skip the npm install if you ran that already above):
npm install
npm start
- Open another terminal or command window.
Contruct a curl command to search for a task:
curl 'http://localhost:8080/get/searchitem?taskname=<nametosearchfor>'
So for example, if you want to search for todo lists with a name of "hello", your command would be:
curl 'http://localhost:8080/get/searchitem?taskname=hello'
If you want to search for a task name with a space in it, for example "hello world" you will need to use the html code for a space (%20) in your curl command, like this:
curl 'http://localhost:8080/get/searchitem?taskname=hello%20world'
- Open the front end component to-do-list/src/component/TodoData.js, on the line after the comment "//begin here" copy/paste/type the following code:
const [todos, setTodos] = useState([]);
useEffect( () => {
async function fetchData() {
try {
const res = await Axios.get('http://localhost:8080/get/items');
setTodos(JSON.stringify(res.data));
console.log(JSON.stringify(res.data));
} catch (err) {
console.log(err);
}
}
fetchData();
}, []);
return <div>{todos}</div>
-
Note the above code does a number of things, it makes use of the "useEffect" hook in react, and the await keyword, this combination is essentially telling react to wait for a call to a backend service to complete, then proceeds with the rest of the render. Remember that nodeJS is an asynchronous platform, so statements can get executed before data is prepared and ready to return. In the case of our Axios.get above, if we didn't have the await in front of it then the rest of the code will proceed and attempt to render before our response is returned from the backend service. To solve this we said that this function is asynchronous and hence we will receive a response from the backend service before proceeding.
-
An alternative is you could do something like we've seen in other services, store the response in state and update it as the data is returned, an example of this will be used in the search functionality next.
- Go to the to-do-list directory, if this is the first time running the front-end for this week execute the following:
npm install
npm audit fix --force
- Start the front end by running the following command in the to-do-list directory:
npm start
- Open another terminal or command window and go to the to-do-list/backend directory and run the backend:
npm start
-
Go to a browser and open the front-end, if not open already, http://localhost:3000, this should bring up the home page. Go to the top navigation bar and click on the "TodoPage"
-
Notice on page load that the top of the page is populated with all of your tasks, saved from the backend service.
- Open the front end component to-do-list/src/component/SearchTodo.js, on the line after the comment "//begin here" copy/paste/type the following code:
e.preventDefault();
// HTTP Client to send a GET request
Axios({
method: "GET",
url: "http://localhost:8080/get/searchitem",
headers: {
"Content-Type": "application/json"
},
params: {
taskname: this.state.content
}
}).then(res => {
this.setState({
tmpdata: JSON.stringify(res.data),
});
});
- Some things to note, there are some UI components defined in this file, the main things they will do is submit a form which will trigger the call to the searchitem backend service, as part of that submit we will take the name of the Todo to search for from the "this.state.content" parameter, the a user would type in the UI text box.
- Note also we have state associated with this component "tmpdata", this state will be set to the data returned from the backend service via the "this.setState({tmpdata: JSON.stringify(res.data),});" code we just put in the HandleSubmit method.
- We will use this state in the render function, underneath the search UI components you'll see "{this.state.tmpdata}", this is empty initially, because we haven't searched for anything, but once you supply a search parameter and click the "Search" button, we will set the state in the HandleSubmit, which will then update the state in our div to hold the return data from the backend service for the search.
- Go to the to-do-list directory, if this is the first time running the front-end for this week execute the following:
npm install
npm audit fix --force
- Start the front end by running the following command in the to-do-list directory:
npm start
- Open another terminal or command window and go to the to-do-list/backend directory and run the backend:
npm start
-
Go to a browser and open the front-end, if not open already, http://localhost:3000, this should bring up the home page. Go to the top navigation bar and click on the "SearchPage" link.
-
Notice the input text box and button that will search for a Todo list in the backend. Type a task name that you know exists or doesn't exist and click the button. (Note if you left the frontend and backend services running after completing the lab steps above make sure you refresh the page so the changes you made load correctly in the browser)
-
Observe the returned value in the div section below the search UI, it will be updated in real-time after we submit the form, returning with the data obtained from the backend.
Note for this section you'll need an IBM Cloud account, so this might be something to try in later weeks when you will provision IBM Cloud accounts
-
Log in to IBM Cloud with your free/trial account.
-
Click "Catalog" along the top right of the page.
-
In the search bar type in "cloudant" and select the first option returned (the cloudant service).
-
Accept all the defaults and scroll to the bottom, the "Lite" plan should be selected which on the right side of the page shows as "free".
-
Click the "Create" button on the lower right side of the page.
-
The cloudant DB will create and make take some minutes to provision. You can view your cloudant resource from the hamburger menu on the top left -> "Resource List", then expand "Databases", your instance will be there and you can monitor it's provisioning progress, when it has a Status of "Active" then it's good to use.
-
Select your Cloudant DB from this page, you will now see a display for managing your cloudant DB. Copy the value for "External endpoint (preferred)".
-
Go to the left side tab and select "Service credentials", now click the "New credential" button, you can specify any name or the default, and select a role of "writer" for now, then click "Add".
-
Once your service credential is created expand it and you should see a number of lines of information, you'll want to copy the value of "apikey", for example:
"apikey": "cwo1uoJqYL-I8jb_rDTL333XCZFwu_T2yWVSOHvp_XK_",
We will want to copy the value:
cwo1uoJqYL-I8jb_rDTL333XCZFwu_T2yWVSOHvp_XK_
- Go to the backend directory and type:
npm install @ibm-cloud/cloudant
-
Create a cloudant credential with a role of 'writer', get API key from cloud console, use the drop down and copy the "apikey" field value
-
Set our cloudant environment variables, in a command window (this process may vary depending on what type of shell you're using) type the following (inserting the values you copied in the previous section):
CLOUDANT_URL= <the value from step 7 in the previous section>
export CLOUDANT_URL
CLOUDANT_APIKEY=<the value from step 9 in the previous section>
export CLOUDANT_APIKEY
- Add the following to end of server.js after the '// Add initDB function here' code block:
async function initDB ()
{
//TODO --- Insert to create DB
//See example at https://www.npmjs.com/package/@ibm-cloud/cloudant#authentication-with-environment-variables for how to create db
try {
const todoDBName = "tododb";
const client = CloudantV1.newInstance({});
const putDatabaseResult = (
await client.putDatabase({
db: todoDBName,
})
).result;
if (putDatabaseResult.ok) {
console.log(`"${todoDBName}" database created.`);
}
} catch (err) {
console.log(
`Cannot create "${todoDBName}" database, err: "${err.message}".`
);
}
};
- Add toward the top of server.js under the "//Init code for Cloudant" comment
const {CloudantV1} = require('@ibm-cloud/cloudant');
if (useCloudant)
{
initDB();
}
- start the backend
npm start
- In server.js near the top, set the useCloudant value from 'false' to 'true', like so:
const useCloudant = true;
-
What happened? You likely got an error stating "Access is denied due to invalid credentials.", if you look at the cloudant IAM roles documentation "writer" does not have permission to create databases. Go to the cloudant management page in IBM cloud, create a new credential with a role of "manager". Copy the apikey value from this new role and set your environment variable to it.
-
stop the backend service, ctrl-c in the window its running in
-
Start the backend service again:
npm start
- You should now see the database being create on startup:
npm start
> [email protected] start
> node server.js
Backend server live on 8080
"tododb" database created.
- In the server.js file add the following to the addItem function after the '//begin here for cloudant' code block:
// Setting `_id` for the document is optional when "postDocument" function is used for CREATE.
// When `_id` is not provided the server will generate one for your document.
const todoDocument = { _id: id.stringify };
// Add "name" and "joined" fields to the document
todoDocument['task'] = task;
todoDocument.curDate = curDate;
todoDocument.dueDate = dueDate;
// Save the document in the database with "postDocument" function
const client = CloudantV1.newInstance({});
console.log('Writing to: ', todoDBName)
const createDocumentResponse = await client.postDocument({
db: todoDBName,
document: todoDocument,
});
console.log('Successfully wrote to cloudant DB');
- in the server.js file we're going to add code for cloudant to retrieve, but in an if/else block, if we are using cloudant go to cloudant to retrieve, otherwise use the local file as before. To make these changes easily, replace the entire getItems function in server.js as follows:
//** week 6, get all items from the json database*/
app.get("/get/items", getItems)
async function getItems (request, response) {
//begin here
//begin cloudant here
if (useCloudant) {
//add for cloudant client
const client = CloudantV1.newInstance({});
var listofdocs;
await client.postAllDocs({
db: todoDBName,
includeDocs: true
}).then(response => {
listofdocs=response.result;
});
response.json(JSON.stringify(listofdocs));
}
else {
//for non-cloudant use-case
var data = await fsPromises.readFile("database.json");
response.json(JSON.parse(data));
}
};
-
create index and design document in cloudant
-
In server.js replace your searchItems function code as follows:
//** week 6, search items service */
app.get("/get/searchitem", searchItems)
async function searchItems (request, response) {
//begin here
var searchField = request.query.taskname;
if (useCloudant){
const client = CloudantV1.newInstance({});
var search_results
await client.postSearch({
db: todoDBName,
ddoc: 'newdesign',
query: 'task:'+searchField,
index: 'newSearch'
}).then(response => {
search_results=response.result;
console.log(response.result);
});
console.log(search_results);
response.json(JSON.stringify(search_results));
}
else {
var json = JSON.parse (await fsPromises.readFile("database.json"));
var returnData = json.filter(jsondata => jsondata.Task === searchField);
response.json(returnData);
}
};
- If you haven't already set useCloudant to 'true', in server.js near the top, set the useCloudant value from 'false' to 'true', like so:
const useCloudant = true;
- Stop the backend server if not already stopped with a cntrl-c, go to the backend directory on a command window and type:
npm start
- Start the front-end UI if not already started, from a separate command window go to the top level directory to-do-list, and run:
npm start
-
Go to the browser and open the front-end url: localhost:3000
-
Try to add a todo item, you should see a message in the backend console after it's added:
Writing to: tododb
Successfully wrote to cloudant DB
-
Click on the TodoPage menu link at the top of the webpage:
-
Click on the SearchPage menu link at the top of the webpage, input a task name to search for and observe results returned from cloudant:
What is a REST API https://www.redhat.com/en/topics/api/what-is-a-rest-api
Rest APIS https://www.ibm.com/cloud/learn/rest-apis
Microservices Architecture https://www.ibm.com/cloud/architecture/architectures/microservices
Modernizing Applications https://www.ibm.com/cloud/architecture/architectures/application-modernization