Lab 1: Nodes
Released: September 16th, 4:00pm ET
Due: September 23rd 3:00pm ET (Before Content Creation Lab)
Introduction
You’re overwhelmed by the vast collection of donuts available. You’re lost, and unfortunately there is no way to traverse the universe of donuts. You’re unable to manage the mess of hyperdonuts and find yourself lost in hyperspace! Looks like you’ll need to come up with a means of orienting yourself to find the most scrumptious donut.
In the Nodes
unit, you will create a node file system - a standard way of managing and navigating between Hypertext nodes. At the end, you will have built a full stack application that will allow users to create, read, delete, and move Hypermedia nodes.
Lab Objective: The objective for this lab is to explore the MERN stack from frontend to backend. You will go through the entire process of connecting to a MongoDB database, creating and inserting a node into the Mongo database, using Express middleware to make calls between the frontend and backend, and lastly visualizing the node’s content and other relevant information as a React component in your web application.
Checklist
Accepting the Assignment
Accept the assignment here. Once you’ve accepted your GitHub repo, clone it locally into your desired root folder - and you should be ready to get going!
nodes-<yourGitHubUsername>
Demo
You can find a demo of the MyHypermedia application here.
Note: It is entirely possible that there will be some unexpected behavior in this application as it is not set up to handle concurrency properly. This means that the application might behave weirdly if multiple users are interacting with the demo at the same time.
More Typescript (.ts) / Javascript
These are some important Typescript / Javascript technologies that you will come across during the lab. We understand that they can be confusing at first, so please ask if anything doesn’t make sense once you use it!
What are HTTP Requests?
The Hypertext Transfer Protocol (HTTP) is designed to enable communication between clients and servers. An HTTP request is made by a client, to a named host, which is located on a server. The aim of the request is to access a resource on the server.
In this assignment, you will use four different types of HTTP requests, they are as follows:
URL params: The :
indicates that the following value is a URL parameter. In other words, :nodeId
is a placeholder that indicates a nodeId
should be placed in its stead.
Note: The definitions and use-cases for each of these request types are flexible. There are industry standards and guidelines but at this stage, we don’t need to know anything more than what is shown in the table above.
What is a Promise<T>
?
JavaScript can have asynchronous functions that don’t return a value immediately, but instead return a Promise
that they will eventually return a value. Sounds a bit tricky – let’s see some examples of asynchonous behavior.
What is “asynchronous”?
Simply put, it means that JavaScript can move on to the next line of code before the previous line finishes executing. Here is an example:
console.log('first print')
setTimeout(() => console.log('second print'), 3000)
console.log('third print')
If you run this snippet of code in an editor, the console would produce the following message:
first print
third print
second print
When we tell the JavaScript to hold off for 3 seconds to print the second print
, JavaScript didn’t wait. JavaScript added it into its “task list”, marked it with execute this task 3 seconds later, and went ahead to execute the third task, which is printing third print
. third print
got printed immediately, and that’s why second print
comes after the third print
. With the same reasoning, do you think the following snippet would achieve it’s goal?
const apiResult = fetch('https://api.github.com/repos')
console.log(apiResult)
In fact, it doesn’t do what we want. Instead, it printed out
[object Promise] { ... }
in the console. Why? Becuase JavaScript didn’t wait, and printed out whatever apiResult
was before the fetch completed. You see, apiResult
is a Promise - Promises are effectively a wrapper around the actual return type, denoting that JavaScript is expecting a object to be returned, and promises that the object will either be returned (resolved), or it will raise an error (rejected). It takes time to get the Promise resolved. In TypeScript, we denote a promise by Promise<T>
, where T
refers to some generic type (could be a string
, object
, INode
, etc.).
In the next section, we will teach you how to print the API result properly. If you have time, you can learn more about the JavaScript event loop: What the heck is the event loop anyway?
async
function & await
keyword
async
and await
allow you to write asynchronous code the same way you write synchonous code
async
function
First of all we have the async
keyword, which you put in front of a function declaration to turn it into an async function.
Example of an async
function vs regular function
Regular function:
const donutPun = (): string => {
return "I donut know any puns..."
};
donutPun();
async
Typescript function:
const donutPun = async (): Promise<string> => {
return "I donut know any puns..." };
}
donutPun();
This is one of the traits of async functions — their return values are guaranteed to be converted to promises. In the second example, the return value would be of type Promise<string>
. So the async keyword is added to functions to tell them to return a promise rather than directly returning the value.
await
keyword
The advantage of an async
function only becomes apparent when you combine it with the await keyword. await
only works inside async
functions.
await
can be put in front of any async promise-based function to pause your code on that line until the promise fulfills, then return the resulting value.
Adding await
in front of a function, for example when we are retrieving from the database, is essentially telling JavaScript to wait until you have a response from the promise.
Now, you should know how to properly print the data that the GitHub API returns:
function fetchAPI = async () => {
const apiResult = await fetch('https://api.github.com/repos')
console.log(apiResult)
}
Architecture
This diagram shows the hierarchy of our full stack application. The interactions between each of the components of our system may seem complex at first, so refer to this diagram when implementing different parts of the .
Disclaimer: You will only have to implement sections when implementing this lab. In Assignment 1, you will be creating your own Mongo database!
Types
In the src
folder on both the server
and client
you will find a types
folder. This contains all of the interfaces that our hypermedia system will use.
Note: We will be using a naming convention where all typescript interfaces are prefaced with an I
for interface. This helps code readability when you have both the interface and the implementation of that interface referenced in one file.
INode
A node is a generic representation of in our hypertext corpus. In this assignment, we will only be implementing a generic node service which manages the location of nodes in a file tree, and supports basic content for text
and image
nodes. To be clear, we are making a notable design decision by choosing to organize nodes via a file tree. This isn’t the only option when building a hypertext system, but it is what you will be implementing in this assignment. At its core, any file tree is just a hierarchy of nodes. Let’s start with how we are defining “node” in the context of a our Hypertext corpus:
interface INode {
title: string
type: NodeType
content: string
nodeId: string
filePath: IFilePath
dateCreated?: Date
}
The ?
in dateCreated?
means that it is an optional field in the INode interface. This means that an INode object can either have that field or not.
IFilePath
The file path type is a simple interface which allows you to easily access an INode’s location. This is important, as you will frequently interact with file paths. Here is the definition for the IFilePath interface:
interface IFilePath {
path: string[]
children: string[]
}
Example:
a root node with nodeId root
who has two children, child1
and child2
would have the following filePath
:
{ path: ['root'], children: ['child1, child2'] }
child1
filePath
{ path: ['root', 'child1'], children: [] }
The children
array will only contain the nodeId
s of their immediate children - which excludes grandchildren or great grandchildren.
IServiceResponse
The IServiceResponse
is an interface we’ve created for this course to make it easier to understand if a function succeeds or fails. If a function fails, the ServiceResponse
will be a failureServiceResponse
that contain an error message. If it succeeds, the ServiceResponse will be a successfulServiceResponse
that contains a payload of type T
, which is a generic type that could be anything (eg. string, INode, etc.) indicated in the angular brackets.
The IServiceResponse
interface enables us to easily determine if a function succeeded, and if it fails, to pass an error message to the calling function.
interface IServiceResponse<T> {
success: boolean;
message: string;
payload: T;
}
Here we see that if createNode()
executes successfully, it will return a ServiceResponse
with a payload of the created node (of type INode
).
function createNode(node: INode): Promise<IServiceResponse<INode>>;
Backend
Backend File Structure
Once you cd
into the server
folder, you will find the following file structure.

NodeRouter.ts
On the backend, you will be using the Express router found in NodeRouter.ts
to receive HTTP requests from the frontend, make corresponding changes to the database, and give the frontend updated data to show the user. Once receiving an HTTP request, the NodeRouter
will call on an appropriate method in NodeGateway
.
Note: NodeRouter
listens to all HTTP requests to /node
, therefore MyRouter.post('/create', async...{})
actually corresponds to /node/create
. For a more detailed explanation, please look at server/src/app.ts
to see how NodeRouter
is configured.
NodeGateway.ts
NodeGateway
handles requests from NodeRouter
, and calls on methods in NodeCollectionConnection
to interact with the database. It contains the complex logic to check whether the request is valid, before modifying the database.
For example, before insertion, NodeGateway.createNode()
will check whether the database already contains a node with the same nodeId
, as well as the the validity of node’s file path. The NodeCollectionConnection.insertNode()
method, on the otherhand, simply receives the node object, and inserts it into the database.
NodeCollectionConnection.ts
NodeCollectionConnection
acts as an in-between communicator between the nodes
collection in the MongoDB database and NodeGateway
. NodeCollectionConnection
defines methods that interact directly with MongoDB. That said, it does not include any of the complex logic that NodeGateway has.
For example, NodeCollectionConnection.deleteNode()
will only delete a single node. Whereas NodeGateway.deleteNode()
deletes all its children from the database as well.
Note: In the backend part of this lab you will implement /node/create
in NodeRouter.ts
, createNode
in NodeGateway.ts
and insertNode
in nodeCollectionConnection.ts
. This is to give you a better understanding of the Express, Node.js and MongoDB stack end-to-end. In Assignment 1 however, to make the workload more managable, you will only be required to implement methods in NodeGateway.ts
as it handles the majority of the logic.
Step 1: Connect Backend to MongoDB
To permanently store our Hypermedia nodes such that they can be accessed using our web application no matter what computer they are on, we need to use a database service. In this class, we will be using MongoDB to store our data.
For this lab, you will be connecting to a database that we have created for you - a database that is shared with everyone in the course. In Assignment 1, you will create and deploy your own MongoDB database!
TODO:
cd
into your server
directory and add a .env
file such that its filepath is: server/.env
- Copy and paste the following text into this newly created
.env
file:
DB_URI = 'mongodb+srv://student:cs1951v@cs1951v-cluster.tjluq.mongodb.net/cs1951v-database?retryWrites=true&w=majority'
PORT=5000
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true
So what did we just do with this .env
file? Well, to connect to a database, MongoDB requires a URI that contains the unique address of the database, a username and password. This way, only those with the correct login details and unique database address can access a database. DB_URI
in this .env
file is that URI - ‘student’ is the username, ‘cs1951v’ is the password and ‘@cs1951v-cluster.tjluq.mongodb.net/cs1951v-database.’ is our database’s unique address. We store this information in a .env
file so that this information does not lie in source code, code that is pushed online for potentially many to see. We use the dotenv
node package to access the URI stored in this .env
. Note, we have added .env
to our .gitignore
file so that private security details are not pushed online.
Step 2: Verify that Stencil Code Runs
Before we start writing backend code, it’s important check if your backend server runs:
TODO:
Run the following yarn
scripts to install the relevant dependencies and start the server:
yarn install && yarn start
To verify that your server is running, if you go to http://localhost:5000
in your browser where you should see MyHypermedia Backend Service
. If you don’t see this message appear in your browser, please reach out to a TA!
Backend Implementation Roadmap
In this lab, you will be implementing the functionality for creating a node, starting with the backend and then finally rendering it as a React component on the frontend. The image below is a high level overview of how we will create a node on the backend.
In this lab, we want to insert both a text and image node into our database describing the most delicious donut in the world (debatable), the Boston Cream Donut.
Step 3: Creating an INode JSON Object
Based on the INode interface that we introduced above, this is how a node like this would look when written as an object:
{
"node": {
"title": "Boston Cream Donut",
"nodeId": "image.u1deL6",
"type": "image",
"content": "https://bakerbynature.com/wp-content/uploads/2021/08/Boston-Cream-Donuts-1-1-of-1.jpg",
"filePath": {
"path": [
"image.u1deL6"
],
"children": []
}
}
}
Note: nodeId
is normally a randomly generated globally unique id (guid) on the client side; In this course, we use image.<guid>
to represent some such unique id on an image node.
TODO:
- Following the example above, create a
INode
object with "type": "text"
(in an empty file), the content of it could be anything you would like, for example, it could be a bio that explains the Boston Cream Donut. The JSON should be formatted in the same way as the Boston Cream Donut
INode shown above.
- Generate an 8 character unique ID here to be used as the new guid.
- Give the
Boston Cream Donut
INode above a new guid such that:
"nodeId": "image.<new-guid>"
"filePath": {"path": ["image.<new-guid>"], ...}
Keep ahold of these JSON objects in a text file, on paper, or anywhere that is most convenient for you - we will be needing them soon.
So why did we just create these random JSON objects? Well, servers and clients communicate with each other over HTTP requests and responses all the time. Sometimes, they need to send data in the form of objects and we oftentimes use the JSON (JavaScript Object Notation) format to represent this data.
In this lab, we will be sending our backend these 2 JSON objects attached to separate HTTP requests. Our backend server will then read this data and convert these JSON objects into a proper INode
object in our TypeScript environment.
Now, onto creating those HTTP requests to send to our backend!
Step 4: HTTP requests with Postman
Postman is a free tool which helps developers run and debug API requests - in other words, it simulates a client and send HTTP requests to our NodeRouter. This is a quick and easy way to make HTTP requests when you don’t have a client set up yet and want to interact with your backend server.
TODO:
- Create an account
- Download the Postman desktop app.
- Sign in to the Postman desktop app with the account you just created.
- Navigate to
My Workspaces
, create a new collection called cs1951v
and add a new request to this collection. Then can copy the image
INode JSON object from Step 3 and ensure your Postman setup looks as follows…
- Note: remember to have changed the guid and have the
nodeId
and filePath
fields reflect this change. Go back to TODO #2
in Step 3 if you have not done so yet.
Your Postman should be set up such that the following is true:
- You have
POST
selected as the request type
- The request URL is:
http://localhost:5000/node/create
- You have
Body
, raw
, and JSON
selected in the respective places.
TODO:
Once everything is set up - click Send
. This will send the HTTP POST request to the backend server that you are running locally at localhost:5000
.
Oh no! You received a FailureServiceResponse
object,[NodeRouter] node/create not implemented
, move on to Step 5 to make sure that our server accepts the POST request!
Step 5: NodeRouter.ts
To implement the post method on the Express router for /node/create
navigate to server/src/nodes/NodeRouter.ts
. You can see that nearly all methods in the file take in two parameters, res
and req
:
req
is a Request
object that we receive from the client.
res
is a Response
object that we send back to the client indicating the status of the request along with an optional JSON payload that contains any requested information.
As we saw in Postman (Step 4), we are sending a POST request to http://localhost:5000/node/create. Therefore we need to set up our router to listen to a POST request at http://localhost:5000/node/create:
MyRouter.post('/create', async (req: Request, res: Response) => {
})
Note: NodeRouter
listens to all HTTP requests to /node
, therefore MyRouter.post('/create', async...{})
actually corresponds to /node/create
. For a more detailed explanation, please look at server/src/app.ts
to see how NodeRouter
is configured.
Now that we have the router listening to the proper route, let’s read the JSON object that was attached to our Postman’s POST request:
MyRouter.post('/create', async (req: Request, res: Response) => {
const node = req.body.node
})
Using console.log()
to verify that node
has been received:
- insert
console.log(node)
after line 2
- start the server with
cd server
, yarn install
and yarn start
- send the POST request via Postman, you should see the JSON object appear like this:
{
title: 'Boston Cream Donut',
nodeId: 'image.guidasdf',
type: 'image',
content: 'https:
filePath: { path: [ 'image.guidasdf' ], children: [] }
}
Awesome! Our HTTP POST request sent via Postman was received and our NodeRouter has succesfully parsed the JSON INode object attached to the POST request.
Now let’s check to see if node
is of type node. We use isINode()
to do this.
MyRouter.post('/create', async (req: Request, res: Response) => {
const node = req.body.node
if (!isINode(node)) {
} else {
}
})
Let’s fill in the HTTP status code into our response.
MyRouter.post('/create', async (req: Request, res: Response) => {
const node = req.body.node
if (!isINode(node)) {
res.status(400).send('not INode!')
} else {
res.status(200).send('is INode!')
}
})
Now, if the node is in the correct shape, we should call on NodeGateway
method to insert it into database.
MyRouter.post('/create', async (req: Request, res: Response) => {
const node = req.body.node
console.log('[NodeRouter] create')
if (!isINode(node)) {
res.status(400).send('not INode!')
} else {
const response = await this.nodeGateway.createNode(node)
res.status(200).send(response)
}
})
Almost there - to make sure our router doesn’t fail on unexpected errors, let’s add a try/catch
to make sure (try/catch
is a common programming pattern that allows catching and handling errors).
MyRouter.post('/create', async (req: Request, res: Response) => {
try {
const node = req.body.node
console.log('[NodeRouter] create')
if (!isINode(node)) {
res.status(400).send('not INode!')
} else {
const response = await this.nodeGateway.createNode(node)
res.status(200).send(response)
}
} catch (e) {
res.status(500).send(e.message)
}
})
For our NodeRouter methods we want to use a try/catch
block so we can catch any uncaught errors and return that error message to the frontend. Note, for all caught errors, our backend should return a failureServiceResponse
.
Note: failureServiceResponse
is a method that we have created in IServiceResponse.ts
to return an IServiceResponse object where success: false
. The same is true for successfulServiceResponse
except that success: true
.
Please have a look at server/src/types/IServiceResponse.ts
for the actual schema of IServiceResponse objects.
Step 6: NodeGateway.ts
Now that we have written /create
for NodeRouter and called NodeGateway.createNode()
, let’s move onto the next step and write NodeGateway.createNode()
.
We start with an empty method:
async createNode(node: any): Promise<IServiceResponse<INode>> {
return failureServiceResponse(
'TODO: [NodeGatway] createNode is not implemented'
)
}
First, we want to check if our argument is of type INode. As a rule of thumb, each method is responsible for error checking its own inputs. Let’s return a failureServiceResponse
with an appropriate failure message if node
is not a valid INode.
async createNode(node: any): Promise<IServiceResponse<INode>> {
const isValidNode = isINode(node)
if (!isValidNode) {
return failureServiceResponse('Not a valid node.')
}
return null
}
Next, we want to check if a node with node.nodeId
already exists in the database. We don’t want duplicate nodeId
s in the database. Let’s use NodeCollectionConnection
’s findNodeById()
method which has already been written for us to verify if nodeId
already exists in the database. We return a failureServiceResponse
if a duplicate already exists.
async createNode(node: any): Promise<IServiceResponse<INode>> {
const isValidNode = isINode(node)
if (!isValidNode) {
return failureServiceResponse('Not a valid node.')
}
const nodeResponse = await this.nodeCollectionConnection.findNodeById(
node.nodeId
)
if (nodeResponse.success) {
return failureServiceResponse(
'Node with duplicate ID already exist in database.'
)
}
return failureServiceResponse(
'TODO: [NodeGatway] createNode is not implemented'
)
}
The nodes are organized in a tree structure such that root nodes contain child nodes that then contain other child nodes, etc. This means that when we are creating a node, we want to make sure that the parent we will attach this new node exists. We can access the node’s parentId by indexing node.filePath.path[nodePath.path.length - 2]
. Note that root nodes will not have a parentId
. Let’s put this into code:
async createNode(node: any): Promise<IServiceResponse<INode>> {
const isValidNode = isINode(node)
if (!isValidNode) {
return failureServiceResponse('Not a valid node.')
}
const nodeResponse = await this.nodeCollectionConnection.findNodeById(
node.nodeId
)
if (nodeResponse.success) {
return failureServiceResponse(
'Node with duplicate ID already exist in database.'
)
}
const nodePath = node.filePath
if (nodePath.path.length > 1) {
const parentId = nodePath.path[nodePath.path.length - 2]
const parentResponse = await this.nodeCollectionConnection.findNodeById(
parentId
)
if (!parentResponse.success) {
return failureServiceResponse('Node has invalid parent')
}
}
return failureServiceResponse(
'TODO: [NodeGatway] createNode is not implemented'
)
}
Now if our node’s filePath
disagrees with its parent’s filePath
, we want to send a failure response:
async createNode(node: any): Promise<IServiceResponse<INode>> {
const isValidNode = isINode(node)
if (!isValidNode) {
return failureServiceResponse('Not a valid node.')
}
const nodeResponse = await this.nodeCollectionConnection.findNodeById(
node.nodeId
)
if (nodeResponse.success) {
return failureServiceResponse(
'Node with duplicate ID already exist in database.'
)
}
const nodePath = node.filePath
if (nodePath.path.length > 1) {
const parentId = nodePath.path[nodePath.path.length - 2]
const parentResponse = await this.nodeCollectionConnection.findNodeById(
parentId
)
if (
!parentResponse.success ||
parentResponse.payload.filePath.path.toString() !==
nodePath.path.slice(0, -1).toString()
) {
return failureServiceResponse('Node has invalid parent / file path.')
}
}
return failureServiceResponse(
'TODO: [NodeGatway] createNode is not implemented'
)
}
Now we need to add node.nodeId
to the parent’s children
field.
async createNode(node: any): Promise<IServiceResponse<INode>> {
const isValidNode = isINode(node)
if (!isValidNode) {
return failureServiceResponse('Not a valid node.')
}
const nodeResponse = await this.nodeCollectionConnection.findNodeById(
node.nodeId
)
if (nodeResponse.success) {
return failureServiceResponse(
'Node with duplicate ID already exist in database.'
)
}
const nodePath = node.filePath
if (nodePath.path.length > 1) {
const parentId = nodePath.path[nodePath.path.length - 2]
const parentResponse = await this.nodeCollectionConnection.findNodeById(
parentId
)
if (
!parentResponse.success ||
parentResponse.payload.filePath.path.toString() !==
nodePath.path.slice(0, -1).toString()
) {
return failureServiceResponse('Node has invalid parent / file path.')
}
parentResponse.payload.filePath.children.push(node.nodeId)
const updateParentResp = await this.updateNode(
parentResponse.payload.nodeId,
[{ fieldName: 'filePath', value: parentResponse.payload.filePath }]
)
if (!updateParentResp.success) {
return failureServiceResponse(
'Failed to update parent.filePath.children.'
)
}
}
return failureServiceResponse(
'TODO: [NodeGatway] createNode is not implemented'
)
}
And finally, once all checks have been completed and the parent node’s filePath.children
field has been updated, we can insert this new node into the database using NodeCollectionConnection.insertNode()
. We then return the resulting response so NodeCollectionConnection
’s response is passed onto NodeRouter
.
async createNode(node: any): Promise<IServiceResponse<INode>> {
const isValidNode = isINode(node)
if (!isValidNode) {
return failureServiceResponse('Not a valid node.')
}
const nodeResponse = await this.nodeCollectionConnection.findNodeById(
node.nodeId
)
if (nodeResponse.success) {
return failureServiceResponse(
'Node with duplicate ID already exist in database.'
)
}
const nodePath = node.filePath
if (nodePath.path.length > 1) {
const parentId = nodePath.path[nodePath.path.length - 2]
const parentResponse = await this.nodeCollectionConnection.findNodeById(
parentId
)
if (
!parentResponse.success ||
parentResponse.payload.filePath.path.toString() !==
nodePath.path.slice(0, -1).toString()
) {
return failureServiceResponse('Node has invalid parent / file path.')
}
parentResponse.payload.filePath.children.push(node.nodeId)
const updateParentResp = await this.updateNode(
parentResponse.payload.nodeId,
[{ fieldName: 'filePath', value: parentResponse.payload.filePath }]
)
if (!updateParentResp.success) {
return failureServiceResponse(
'Failed to update parent.filePath.children.'
)
}
}
const insertNodeResp = await this.nodeCollectionConnection.insertNode(node)
return insertNodeResp
}
Great! We just finished NodeGateway.createNode()
. Let’s move onto NodeCollectionConnection.insertNode()
.
Step 7: NodeCollectionConnection.ts
Last thing we need to do in the backend, is to interface with the database! Let’s start by checking whether the input is a valid INode
.
async insertNode(node: INode): Promise<IServiceResponse<INode>> {
if (!isINode(node)) {
return failureServiceResponse(
'Failed to insert node due to improper input ' +
'to nodeCollectionConnection.insertNode'
)
}
}
After this, let’s call the relavant MongoDB function, and return the inserted document on success.
async insertNode(node: INode): Promise<IServiceResponse<INode>> {
if (!isINode(node)) {
return failureServiceResponse(
'Failed to insert node due to improper input ' +
'to nodeCollectionConnection.insertNode'
)
}
const insertResponse = await this.client
.db()
.collection(this.collectionName)
.insertOne(node)
if (insertResponse.insertedCount) {
return successfulServiceResponse(insertResponse.ops[0])
}
return failureServiceResponse(
'Failed to insert node, insertCount: ' + insertResponse.insertedCount
)
}
Step 8: Run Backend Test
Great! So now that we have fully implemented our system’s create node functionality, let’s run some unit tests to see if our code works as intended.
Backend Tests: There are two types of tests: system tests and unit tests.
- System tests test our entire system end-to-end (e2e) as if we were using the system as a user. This interacts with a real MongoDB database to simulate real world use. System tests can be found at
server/src/test/Nodes/e2e
.
- Unit tests test each method in our program one by one such that we are absolutely sure that each method works as intended. We normally only test public/exposed methods - helper methods will not be unit tested. Unit tests can be found at
server/src/test/Nodes/Mock
. Note, we use ‘Mock’ as it symbolizes the ‘Mock’ database we use for unit testing - a virtual database we simulate in our computer’s local memory.
TODO:
- Run
cd server
and yarn labtest
in your terminal to see whether it passes the tests!
If all tests pass, you are all set on the backend! Now let’s move onto the frontend portion of this lab.
Frontend
Introduction
Now that we have a working backend that accepts INode objects from HTTP requests, we should make sure that our client is able to send those HTTP requests.
Once we have a way of creating nodes, we will be implementing two simple React components in this lab to introduce a few key concepts: props, rendering, and components as functions.
Note: We assume a basic level of familarity with HTML going forward.
If you have no experience, we recommend approaching a TA who will be happy to point you towards additional resources!
Frontend Folder Structure

The blue elements are what you will be working on in this lab and assignment. You can search for a file in VSCode using Cmd+P (Mac) or Ctrl+P (Windows).
React
React is a frontend Javascript library for building isolated and state-aware UI components. This makes it ideal for building high performance modern web applications!
Some examples of webpages that you have all used that use React include:
- Facebook
- Instagram
- Netflix
- Zumper
Component Breakdown
A Component is one of the core building blocks of React. In other words, we can say that every application you will develop in React will be made up of pieces called components. Components make the task of building UIs much easier. You can see a UI broken down into multiple individual pieces called components and work on them independently and merge them all in a parent component which will be your final UI.
The beauty of React is that when we change the state of a Component, rather than re-rendering the entire UI’s state, it only updates the t’s state. This is what we mean by Component isolation.
In this image we have broken down the React UI Components of the MyHypermedia Web App that you will be implementing.

In our stencil code, each React component is organized into a folder. Let us say we had a Donut
component. Then it would be a folder called Donut
which contains the following files:
Filename |
Case Style |
Description |
Donut.tsx |
|
Handles the rendering of the component itself and the creation of the |
donutUtils.ts/.tsx |
camelCase |
useState useEffect |
Donut.scss |
PascalCase |
SCSS file with everything relevant to the Donut component that is rendered. |
index.ts |
camelCase |
(eg. import { oreo } from '../../../../../../../donuts/chocolate/oreo' ) |
Verify that your frontend loads
First we should add the .env
to your client
folder so that ESLint works as we expect it to! It should look as follows:
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true
Before we start writing frontend code, it’s important to check that your client runs as expected.
TODO:
Run the following yarn
scripts to install the relevant dependencies and start the React services:
cd client
yarn install && yarn start
To verify that your React service is running, if you go to https://localhost:3000
in your browser then you should see the client web app that we have built MyHypermedia
. If the web app does not load, please shout “TAaaa!!” (in Slack or in CIT219)
Step 1: createNode
in frontend NodeGateway.ts
We want to easily send and retrieve data from our database using our frontend app. Users will interact with data through buttons, forms etc. and we need a way to retrieve data. For that we will use a frontend NodeGateway
to make HTTP requests.
ke our HTTP requests, they are all contained in requests.ts
. For createNode
we want to use a POST method since we are sending data from the client to the server!
createNode: async (node: INode): Promise<IServiceResponse<INode>> => {
try {
const url:string = ??
const body:any = ??
return await post<IServiceResponse<INode>>(url, body)
} catch (exception) {
return failureServiceResponse('[createNode] Unable to access backend')
}
}
Now we need to define the arguments that we are passing into the post request:
-
url
:
When you are running it locally the baseEndpoint
variable https://localhost:5000
.
Since this NodeGateway
only sends HTTP requests to /node...
we use the servicePath
variable to define the permanent prefix attached to the route.
Thus, our url should be:
baseEndpoint + servicePath + '/create'
-
body
:
The body of the POST HTTP request should be the INode object that we are creating.
Finally, we arrive at the final method, which returns a IServiceResponse
Promise.
createNode: async (node: INode): Promise<IServiceResponse<INode>> => {
try {
const url: string = baseEndpoint + servicePath + '/create'
const body: any = { node: node }
return await post<IServiceResponse<INode>>(url, body)
} catch (exception) {
return failureServiceResponse('[createNode] Unable to access backend')
}
}
We can now access that from anywhere in our client
to make an API request to create a node!
import NodeGateway from '../../nodes/NodeGateway'
const nodeResponse = await NodeGateway.createNode(newNode)
You can look into the createNodeFromModal
method in createNodeUtils.ts
to see an example!
Step 2: ReactHooks & opening the CreateNodeModal
Before we do this step, we will introduce two very important in React!
in React. We do this because Functional requires much less boilerplate code, and is now the new industry standard.
React Hooks
First, let’s talk about React Hooks. If you are not familiar with it, the name really doesn’t explain anything at all, but we hope you will gradually like it as you get to know it more.
You can identify a React Hook by its names - they usually have use
as a prefix to their name. Essentially, React Hooks deal with manipulating the state, or the value, of the variable. With hooks, . Some of the really popular React Hooks include useState
and useEffect
, and we will introduce them in this lab.
useState
useState
is the most common hook, and it deals with creating reactive variables - whenever reactive variables get updated, React will automatically the component with the updated value. Imagine we want to have a hitCounter
variable that represents the number of button clicks. We can declare it with the useState
hook:
const [hitCounter, setHitCounter] = useState(0)
The first word, const
, is the reserved keyword for declaring a new variable. However, we are actually declaring 2 things here: a hitCounter
variable of type number
, and a function setHitCounter
that is used to mutate the value of hitCounter
. On the right side of the equal sign, we are calling the useState
hook with the parameter 0
-hitCounter
0
So we’ve used useState
, is that it? Let’s see how we can use these 2 variables to achieve our goal - building a hit counter!
import React, { useState } from "react";
const myCounter = () => {
const [hitCounter, setHitCounter] = useState(0);
return (
<div>
<div>Count: {hitCounter}</div>
<button onClick={() => setHitCounter(hitCounter + 1)}>
Hit Me!
</button>
<div>
);
};
Can you visualize what it would look like? If rendered in browser, there would probably be a Count: 0
along with a button that says Hit Me!
. Let’s observe what is written inside the <button>
.
We see that there is an onClick
event handler written inside the <button>
div. Whatever is written inside the onClick
will be called when user clicks the button. In this case, we are calling setHitCounter
with the parameter hitCounter + 1
to hitCounter
everytime this button is pressed. hitCounter
’s current value is 0
. With some math knowledge, we know that we are effectively putting 1
as an argument into the setHitCounter
; not surprisingly, React will now update hitCounter
’s value to 1
, and the will show Count: 1
. If we keep clicking the button, this process will be repeated, and we can see the hitCounter
value steadily increase.
To recap, useState
instantiates a variable with a default value, and React provides us with a mutator to change the variable’s value. Every update to variable will cause a of the component.
Note that the value of the hitCounter
will be restored to 0 when we refresh the page. That is the anticipated behavior. If we want to make the data persistent through page refresh, we can use a database, or utilize the browser’s local storage.
useEffect
Now, let’s learn another simple hook - useEffect
. We can treat this hook as an if changed
statement. Let’s see what the useEffect
hook looks like.
useEffect(() => { console.log('Button Clicked!') }, [hitCounter])
For readability, we formatted the useEffect
into one-line and commented on each part. A useEffect
hook takes in 2 parameters, the first one being a callback function, and the second parameter is a dependency array.
Callback Function
A callback function is simply a function that’s passed as an argument to another function!
Dependency Array
Dependency array is a very important concept to grasp. Essentially, it is the if
part of the useEffect
hook. We can put variables of different types into the dependency array, and whenever any variable in the dependency array gets updated, the callback function will be executed. For example, in the code above, whenever hitCounter
is updated, the callback function will print to the console. You can put as many variables as you want into the dependency array, but make sure to put in the right ones and avoid endless loops!
There are 2 special cases for the dependency array:
- When the dependency array is empty, such as
useEffect(() => { console.log("hello!) }, [])
, it means the callback function will only be called once on component creation. this mode comes in handy.
- When the dependency array is not supplied, such as
useEffect(() => { console.log("change!")})
, the callback function will be executed whenever the component is rerendered.
With this knowlegde, what does this snippet of code to do?
import React, { useState, useEffect } from "react";
const myCounter = () => {
const [hitCounter, setHitCounter] = useState(0);
useEffect(() => {
console.log("Clicked!")
}, [hitCounter])
return (
<div>
<div>Count: {hitCounter}</div>
<button onClick={() => setHitCounter(hitCounter + 1)}>
Hit Me!
</button>
<div>
);
};
Whenever the button is clicked, the value of the hitCounter
will be changed, triggering a useEffect call. The useEffect will print a string to console.
Basic Rules of React Hook
- The hook can only be used in Functional Component. It’s not compatible with the old Class Component.
- Hooks can only be called at top-level - they cannot be called in loops,
if
conditions, or nested functions.
Now that you have “mastered” React ;) let us move back to the codebase and implement the create
button to open the modal. We use a UI package called Chakra to render our createNodeModal.
Navigate to MainView.tsx
, this is where we render the components and handle logic to load all of the components and the nodes from our database. We have divided the file into 6 sections, they are as follows:
- [1] useState hooks
- [2] Constant variables
- [3] useEffect hooks
- [4] Button handlers
- [5] Helper functions
- [6] JSX component
Step 1: Notice the following useState hook in [1]:
const [createNodeModalIsOpen, setCreateNodeModalIsOpen] = useState(false)
We use a state variable called createNodeModalIsOpen
to keep track of state of the CreateNodeModal
.
If the createNodeModalIsOpen
variable changes, our MainView component will know to re-render the components that depend on the createNodeModalIsOpen
, but how do we change the variable? For that we need to use the setCreateNodeModalIsOpen
function from our useState hook which updates the variable!
Step 2: Go to [6], since the createNode button is located in the Header
component we need the Header
component to be able to access and call the setCreateNodeModalIsOpen
function. How do we do this? We pass it in to the a a prop
(Note: props
stands for “properties”) to the Header
component. You can even pass functions in as props!
We’ll go into more detail on components and props in Step 3!
We should update the onCreateButtonClick
prop in the Header
component so that it looks as follows:
<Header
onHomeClick={handleHomeClick}
onCreateButtonClick={() => setCreateNodeModalIsOpen(true)}
/>
Now our Header
component takes in the method that updates our variable. It knows what to do when the create button is clicked! Try it out, and try creating a node with the CreateNodeModal!
Congratulations! You’ve successfully implemented Create Node, from the backend all the way to the frontend!
Step 3: Implementing ImageContent.tsx
Now that we are able to create a node, what about viewing it? Sure we have an INode object with fields, but how useful is that? We need to take the INode oject and turn it into something useful.
This is what our Boston Cream Donut
currently looks like, not so delicious:
To start doing this, navigate to ImageContent.tsx
.
Here, we see the basic structure of a React component: it’s just a function! It takes an input props
that is an object of shape IContentProps
, and it outputs what it should render (for now, null
).
IContentProps
is actually defined in a different file; you can Cmd+Click (Mac) or Ctrl+Click (Windows) on almost any expression in VSCode to see where it’s defined or used. A lot of people find this extremely useful; try it out!
You can see that IContentProps
has a field called content
of type string
. For an image node, this string is the image url.
A common pattern when programming in React is to get the properties you need from a props
object. Here are two ways to get the content
field from our props
:
const content = props.content
const { content } = props
const { property1, property2 } = myObject
Once you have your content
stored in a const
, you will want to render an HTML image. You can do this by returning an <img />
HTML tag with the src
set to our image url (which is our content
!).
If you haven’t worked with React or JSX before, it might surprise you to see HTML in a Javascript function; essentially, we use (...)
to denote HTML elements, and we use {...}
to denote Javascript when we are within an HTML element.
For example:
return (
<img src={} />
)
Did you notice that <img>
tag can be written in 2 formats?
In JSX, which React uses, you can either use a traditional <img src='...'></img>
to render an image, or to use a self-closing tag that looks like <img src='...' />
. This also applies to other HTML tags as well. They are equivalent expressions and either format is accepted.
When you are done with your ImageContent
component, you should see an image when you click on an image node. Ask a TA if you get stuck!
Step 4: Implementing TextContent.tsx
Now we can implement our text content component in TextContent.tsx
. The content of text nodes is simpler to render: it’s just a string
! You can render a Javascript string
by wrapping it in curly braces and putting it in an HTML tag (like a div
).
For example:
const myString = 'donut!'
return (
<div>{myString}</div>
)
Once you render your text content, you should be see the content text when you click on a text node. Now, you’ll get some practice styling react components using .scss
files!
Styling with SCSS (.scss)
Throughout this class we will be using .scss
files instead of .css files.
SCSS files allows us to write SASS, which is an extension of CSS (Cascading Style Sheets) that adds functionality like variables and nesting. Because of these additional features, SCSS files are often used instead of CSS files in industry.
Note: While there are other alternatives for styling your React components, we’ll be sticking with SCSS for this project. We will also give you the option to use other CSS styling methods such as tailwind
and emotion
later on in this class - or now if you are already familiar with them!
In the same directory, go to TextContent.scss
. Define a SCSS class for your text content – the syntax is like this (though the class can be named anything):
Now, hook up this SCSS class to your React component by adding a className
string to the HTML element you are rendering. You can do this like so:
return (
<div className="<YOUR_CLASSNAME_HERE>">{myString}</div>
)
Ok, you’re now ready to start styling! Some good places to start are font-size
, font-weight
, color
, and padding
. You can add styles like so:
.textContent {
font-size: 10px;
font-weight: bold;
color: red;
padding: 20px;
}
Change these values and add more styles (See https://www.w3schools.com/html/html_styles.asp for some more examples, or do a quick Google search!) until you are happy with how your text is rendered.
Step 5: Rendering ImageContent.tsx and TextContent.tsx
Now that you have implemented TextContent.tsx
and ImageContent.tsx
, we need our NodeView.tsx
to render either a TextContent
or ImageContent
component depending on the type of node selected.
In client/src/components/NodeView/NodeView.tsx
we see this snippet of code returns what node content to render:
<div
className="nodeView-content"
style={{
maxHeight: hasBreadcrumb ? 'calc(100% - 118px)' : 'calc(100% - 72px)',
}}
>
{renderContent(props, type, content)}
</div>
</div>
)
}
Navigate to the implementation of renderContent
by cmd + click
on Mac and ctrl + click
on Windows.
Implement the two TODO
s in this file (nodeViewUtils.tsx
). Pay attention to what props ImageContent
and TextContent
require!
Once done, your frontend should look like this!

Yay! Now you can render the content of text
and image
nodes from our Mongo database!
Congrats! You’re done with the lab; see a TA to get checked off or attend TA Hours before the next lab section (September 23rd 3:00pm ET)

⬅️ Return to course website
Lab 1: Nodes
Released: September 16th, 4:00pm ET
Due: September 23rd 3:00pm ET (Before Content Creation Lab)
Introduction
You’re overwhelmed by the vast collection of donuts available. You’re lost, and unfortunately there is no way to traverse the universe of donuts. You’re unable to manage the mess of hyperdonuts and find yourself lost in hyperspace! Looks like you’ll need to come up with a means of orienting yourself to find the most scrumptious donut.
In the
Nodes
unit, you will create a node file system - a standard way of managing and navigating between Hypertext nodes. At the end, you will have built a full stack application that will allow users to create, read, delete, and move Hypermedia nodes.Lab Objective: The objective for this lab is to explore the MERN stack from frontend to backend. You will go through the entire process of connecting to a MongoDB database, creating and inserting a node into the Mongo database, using Express middleware to make calls between the frontend and backend, and lastly visualizing the node’s content and other relevant information as a React component in your web application.
Checklist
Nodes
GitHub Classroom assignmentBackground Information
createNode
by making changes in the following filesNodeRouter.ts
NodeGateway.ts
NodeCollectionConnection.ts
NodeGateway.ts
text
andimage
nodes.Accepting the Assignment
Accept the assignment here. Once you’ve accepted your GitHub repo, clone it locally into your desired root folder - and you should be ready to get going!
For Lab 1 and Assignment 1 you will be using the same
nodes-<yourGitHubUsername>
repository.Demo
You can find a demo of the MyHypermedia application here.
Note: It is entirely possible that there will be some unexpected behavior in this application as it is not set up to handle concurrency properly. This means that the application might behave weirdly if multiple users are interacting with the demo at the same time.
Background Information
More Typescript (.ts) / Javascript
These are some important Typescript / Javascript technologies that you will come across during the lab. We understand that they can be confusing at first, so please ask if anything doesn’t make sense once you use it!
What are HTTP Requests?
The Hypertext Transfer Protocol (HTTP) is designed to enable communication between clients and servers. An HTTP request is made by a client, to a named host, which is located on a server. The aim of the request is to access a resource on the server.
In this assignment, you will use four different types of HTTP requests, they are as follows:
URL params: The
:
indicates that the following value is a URL parameter. In other words,:nodeId
is a placeholder that indicates anodeId
should be placed in its stead.Note: The definitions and use-cases for each of these request types are flexible. There are industry standards and guidelines but at this stage, we don’t need to know anything more than what is shown in the table above.
What is a
Promise<T>
?JavaScript can have asynchronous functions that don’t return a value immediately, but instead return a
Promise
that they will eventually return a value. Sounds a bit tricky – let’s see some examples of asynchonous behavior.What is “asynchronous”?
Simply put, it means that JavaScript can move on to the next line of code before the previous line finishes executing. Here is an example:
If you run this snippet of code in an editor, the console would produce the following message:
When we tell the JavaScript to hold off for 3 seconds to print the
second print
, JavaScript didn’t wait. JavaScript added it into its “task list”, marked it with execute this task 3 seconds later, and went ahead to execute the third task, which is printingthird print
.third print
got printed immediately, and that’s whysecond print
comes after thethird print
. With the same reasoning, do you think the following snippet would achieve it’s goal?In fact, it doesn’t do what we want. Instead, it printed out
[object Promise] { ... }
in the console. Why? Becuase JavaScript didn’t wait, and printed out whateverapiResult
was before the fetch completed. You see,apiResult
is a Promise - Promises are effectively a wrapper around the actual return type, denoting that JavaScript is expecting a object to be returned, and promises that the object will either be returned (resolved), or it will raise an error (rejected). It takes time to get the Promise resolved. In TypeScript, we denote a promise byPromise<T>
, whereT
refers to some generic type (could be astring
,object
,INode
, etc.).In the next section, we will teach you how to print the API result properly. If you have time, you can learn more about the JavaScript event loop: What the heck is the event loop anyway?
async
function &await
keywordasync
andawait
allow you to write asynchronous code the same way you write synchonous codeasync
functionFirst of all we have the
async
keyword, which you put in front of a function declaration to turn it into an async function.Example of an
async
function vs regular functionRegular function:
async
Typescript function:This is one of the traits of async functions — their return values are guaranteed to be converted to promises. In the second example, the return value would be of type
Promise<string>
. So the async keyword is added to functions to tell them to return a promise rather than directly returning the value.await
keywordThe advantage of an
async
function only becomes apparent when you combine it with the await keyword.await
only works insideasync
functions.await
can be put in front of any async promise-based function to pause your code on that line until the promise fulfills, then return the resulting value.Adding
await
in front of a function, for example when we are retrieving from the database, is essentially telling JavaScript to wait until you have a response from the promise.Now, you should know how to properly print the data that the GitHub API returns:
Architecture
This diagram shows the hierarchy of our full stack application. The interactions between each of the components of our system may seem complex at first, so refer to this diagram when implementing different parts of the assignment.
Disclaimer: You will only have to implement the orange bordered sections when implementing this lab. In Assignment 1, you will be creating your own Mongo database!
Types
In the
src
folder on both theserver
andclient
you will find atypes
folder. This contains all of the interfaces that our hypermedia system will use.Note: We will be using a naming convention where all typescript interfaces are prefaced with an
I
for interface. This helps code readability when you have both the interface and the implementation of that interface referenced in one file.INode
A node is a generic representation of content in our hypertext corpus. In this assignment, we will only be implementing a generic node service which manages the location of nodes in a file tree, and supports basic content for
text
andimage
nodes. To be clear, we are making a notable design decision by choosing to organize nodes via a file tree. This isn’t the only option when building a hypertext system, but it is what you will be implementing in this assignment. At its core, any file tree is just a hierarchy of nodes. Let’s start with how we are defining “node” in the context of a our Hypertext corpus:The
?
indateCreated?
means that it is an optional field in the INode interface. This means that an INode object can either have that field or not.IFilePath
The file path type is a simple interface which allows you to easily access an INode’s location. This is important, as you will frequently interact with file paths. Here is the definition for the IFilePath interface:
Example:
a root node with nodeId
root
who has two children,child1
andchild2
would have the followingfilePath
:{ path: ['root'], children: ['child1, child2'] }
and the
child1
node will have the followingfilePath
:{ path: ['root', 'child1'], children: [] }
The
children
array will only contain thenodeId
s of their immediate children - which excludes grandchildren or great grandchildren.IServiceResponse
The
IServiceResponse
is an interface we’ve created for this course to make it easier to understand if a function succeeds or fails. If a function fails, theServiceResponse
will be afailureServiceResponse
that contain an error message. If it succeeds, the ServiceResponse will be asuccessfulServiceResponse
that contains a payload of typeT
, which is a generic type that could be anything (eg. string, INode, etc.) indicated in the angular brackets.The
IServiceResponse
interface enables us to easily determine if a function succeeded, and if it fails, to pass an error message to the calling function.Here we see that if
createNode()
executes successfully, it will return aServiceResponse
with a payload of the created node (of typeINode
).Backend
Backend File Structure
Once you
cd
into theserver
folder, you will find the following file structure.NodeRouter.ts
On the backend, you will be using the Express router found in
NodeRouter.ts
to receive HTTP requests from the frontend, make corresponding changes to the database, and give the frontend updated data to show the user. Once receiving an HTTP request, theNodeRouter
will call on an appropriate method inNodeGateway
.Note:
NodeRouter
listens to all HTTP requests to/node
, thereforeMyRouter.post('/create', async...{})
actually corresponds to/node/create
. For a more detailed explanation, please look atserver/src/app.ts
to see howNodeRouter
is configured.NodeGateway.ts
NodeGateway
handles requests fromNodeRouter
, and calls on methods inNodeCollectionConnection
to interact with the database. It contains the complex logic to check whether the request is valid, before modifying the database.For example, before insertion,
NodeGateway.createNode()
will check whether the database already contains a node with the samenodeId
, as well as the the validity of node’s file path. TheNodeCollectionConnection.insertNode()
method, on the otherhand, simply receives the node object, and inserts it into the database.NodeCollectionConnection.ts
NodeCollectionConnection
acts as an in-between communicator between thenodes
collection in the MongoDB database andNodeGateway
.NodeCollectionConnection
defines methods that interact directly with MongoDB. That said, it does not include any of the complex logic that NodeGateway has.For example,
NodeCollectionConnection.deleteNode()
will only delete a single node. WhereasNodeGateway.deleteNode()
deletes all its children from the database as well.Note: In the backend part of this lab you will implement
/node/create
inNodeRouter.ts
,createNode
inNodeGateway.ts
andinsertNode
innodeCollectionConnection.ts
. This is to give you a better understanding of the Express, Node.js and MongoDB stack end-to-end. In Assignment 1 however, to make the workload more managable, you will only be required to implement methods inNodeGateway.ts
as it handles the majority of the logic.Step 1: Connect Backend to MongoDB
To permanently store our Hypermedia nodes such that they can be accessed using our web application no matter what computer they are on, we need to use a database service. In this class, we will be using MongoDB to store our data.
For this lab, you will be connecting to a database that we have created for you - a database that is shared with everyone in the course. In Assignment 1, you will create and deploy your own MongoDB database!
TODO:
cd
into yourserver
directory and add a.env
file such that its filepath is:server/.env
.env
file:So what did we just do with this
.env
file? Well, to connect to a database, MongoDB requires a URI that contains the unique address of the database, a username and password. This way, only those with the correct login details and unique database address can access a database.DB_URI
in this.env
file is that URI - ‘student’ is the username, ‘cs1951v’ is the password and ‘@cs1951v-cluster.tjluq.mongodb.net/cs1951v-database.’ is our database’s unique address. We store this information in a.env
file so that this information does not lie in source code, code that is pushed online for potentially many to see. We use thedotenv
node package to access the URI stored in this.env
. Note, we have added.env
to our.gitignore
file so that private security details are not pushed online.Step 2: Verify that Stencil Code Runs
Before we start writing backend code, it’s important check if your backend server runs:
TODO:
Run the following
yarn
scripts to install the relevant dependencies and start the server:yarn install && yarn start
To verify that your server is running, if you go to
http://localhost:5000
in your browser where you should seeMyHypermedia Backend Service
. If you don’t see this message appear in your browser, please reach out to a TA!Backend Implementation Roadmap
In this lab, you will be implementing the functionality for creating a node, starting with the backend and then finally rendering it as a React component on the frontend. The image below is a high level overview of how we will create a node on the backend.
In this lab, we want to insert both a text and image node into our database describing the most delicious donut in the world (debatable), the Boston Cream Donut.
Step 3: Creating an INode JSON Object
Based on the INode interface that we introduced above, this is how a node like this would look when written as an object:
Note:
nodeId
is normally a randomly generated globally unique id (guid) on the client side; In this course, we useimage.<guid>
to represent some such unique id on an image node.TODO:
INode
object with"type": "text"
(in an empty file), the content of it could be anything you would like, for example, it could be a bio that explains the Boston Cream Donut. The JSON should be formatted in the same way as theBoston Cream Donut
INode shown above.Boston Cream Donut
INode above a new guid such that:"nodeId": "image.<new-guid>"
"filePath": {"path": ["image.<new-guid>"], ...}
Keep ahold of these JSON objects in a text file, on paper, or anywhere that is most convenient for you - we will be needing them soon.
So why did we just create these random JSON objects? Well, servers and clients communicate with each other over HTTP requests and responses all the time. Sometimes, they need to send data in the form of objects and we oftentimes use the JSON (JavaScript Object Notation) format to represent this data.
In this lab, we will be sending our backend these 2 JSON objects attached to separate HTTP requests. Our backend server will then read this data and convert these JSON objects into a proper
INode
object in our TypeScript environment.Now, onto creating those HTTP requests to send to our backend!
Step 4: HTTP requests with Postman
Postman is a free tool which helps developers run and debug API requests - in other words, it simulates a client and send HTTP requests to our NodeRouter. This is a quick and easy way to make HTTP requests when you don’t have a client set up yet and want to interact with your backend server.
TODO:
My Workspaces
, create a new collection calledcs1951v
and add a new request to this collection. Then can copy theimage
INode JSON object from Step 3 and ensure your Postman setup looks as follows…nodeId
andfilePath
fields reflect this change. Go back toTODO #2
in Step 3 if you have not done so yet.Your Postman should be set up such that the following is true:
POST
selected as the request typehttp://localhost:5000/node/create
Body
,raw
, andJSON
selected in the respective places.TODO:
Once everything is set up - click
Send
. This will send the HTTP POST request to the backend server that you are running locally atlocalhost:5000
.Oh no! You received a
FailureServiceResponse
object,[NodeRouter] node/create not implemented
, move on to Step 5 to make sure that our server accepts the POST request!Step 5:
NodeRouter.ts
To implement the post method on the Express router for
/node/create
navigate toserver/src/nodes/NodeRouter.ts
. You can see that nearly all methods in the file take in two parameters,res
andreq
:req
is aRequest
object that we receive from the client.res
is aResponse
object that we send back to the client indicating the status of the request along with an optional JSON payload that contains any requested information.As we saw in Postman (Step 4), we are sending a POST request to http://localhost:5000/node/create. Therefore we need to set up our router to listen to a POST request at http://localhost:5000/node/create:
Note:
NodeRouter
listens to all HTTP requests to/node
, thereforeMyRouter.post('/create', async...{})
actually corresponds to/node/create
. For a more detailed explanation, please look atserver/src/app.ts
to see howNodeRouter
is configured.Now that we have the router listening to the proper route, let’s read the JSON object that was attached to our Postman’s POST request:
Using
console.log()
to verify thatnode
has been received:console.log(node)
after line 2cd server
,yarn install
andyarn start
Awesome! Our HTTP POST request sent via Postman was received and our NodeRouter has succesfully parsed the JSON INode object attached to the POST request.
Now let’s check to see if
node
is of type node. We useisINode()
to do this.Let’s fill in the HTTP status code into our response.
Now, if the node is in the correct shape, we should call on
NodeGateway
method to insert it into database.Almost there - to make sure our router doesn’t fail on unexpected errors, let’s add a
try/catch
to make sure (try/catch
is a common programming pattern that allows catching and handling errors).For our NodeRouter methods we want to use a
try/catch
block so we can catch any uncaught errors and return that error message to the frontend. Note, for all caught errors, our backend should return afailureServiceResponse
.Note:
failureServiceResponse
is a method that we have created inIServiceResponse.ts
to return an IServiceResponse object wheresuccess: false
. The same is true forsuccessfulServiceResponse
except thatsuccess: true
.Please have a look at
server/src/types/IServiceResponse.ts
for the actual schema of IServiceResponse objects.Step 6: NodeGateway.ts
Now that we have written
/create
for NodeRouter and calledNodeGateway.createNode()
, let’s move onto the next step and writeNodeGateway.createNode()
.We start with an empty method:
First, we want to check if our argument is of type INode. As a rule of thumb, each method is responsible for error checking its own inputs. Let’s return a
failureServiceResponse
with an appropriate failure message ifnode
is not a valid INode.Next, we want to check if a node with
node.nodeId
already exists in the database. We don’t want duplicatenodeId
s in the database. Let’s useNodeCollectionConnection
’sfindNodeById()
method which has already been written for us to verify ifnodeId
already exists in the database. We return afailureServiceResponse
if a duplicate already exists.The nodes are organized in a tree structure such that root nodes contain child nodes that then contain other child nodes, etc. This means that when we are creating a node, we want to make sure that the parent we will attach this new node exists. We can access the node’s parentId by indexing
node.filePath.path[nodePath.path.length - 2]
. Note that root nodes will not have aparentId
. Let’s put this into code:Now if our node’s
filePath
disagrees with its parent’sfilePath
, we want to send a failure response:Now we need to add
node.nodeId
to the parent’schildren
field.And finally, once all checks have been completed and the parent node’s
filePath.children
field has been updated, we can insert this new node into the database usingNodeCollectionConnection.insertNode()
. We then return the resulting response soNodeCollectionConnection
’s response is passed ontoNodeRouter
.Great! We just finished
NodeGateway.createNode()
. Let’s move ontoNodeCollectionConnection.insertNode()
.Step 7:
NodeCollectionConnection.ts
Last thing we need to do in the backend, is to interface with the database! Let’s start by checking whether the input is a valid
INode
.After this, let’s call the relavant MongoDB function, and return the inserted document on success.
Step 8: Run Backend Test
Great! So now that we have fully implemented our system’s create node functionality, let’s run some unit tests to see if our code works as intended.
Backend Tests: There are two types of tests: system tests and unit tests.
server/src/test/Nodes/e2e
.server/src/test/Nodes/Mock
. Note, we use ‘Mock’ as it symbolizes the ‘Mock’ database we use for unit testing - a virtual database we simulate in our computer’s local memory.TODO:
cd server
andyarn labtest
in your terminal to see whether it passes the tests!If all tests pass, you are all set on the backend! Now let’s move onto the frontend portion of this lab.
Frontend
Introduction
Now that we have a working backend that accepts INode objects from HTTP requests, we should make sure that our client is able to send those HTTP requests.
Once we have a way of creating nodes, we will be implementing two simple React components in this lab to introduce a few key concepts: props, rendering, and components as functions.
Note: We assume a basic level of familarity with HTML going forward.
If you have no experience, we recommend approaching a TA who will be happy to point you towards additional resources!
Frontend Folder Structure
The blue elements are what you will be working on in this lab and assignment. You can search for a file in VSCode using Cmd+P (Mac) or Ctrl+P (Windows).
React
React is a frontend Javascript library for building isolated and state-aware UI components. This makes it ideal for building high performance modern web applications!
Some examples of webpages that you have all used that use React include:
Component Breakdown
A Component is one of the core building blocks of React. In other words, we can say that every application you will develop in React will be made up of pieces called components. Components make the task of building UIs much easier. You can see a UI broken down into multiple individual pieces called components and work on them independently and merge them all in a parent component which will be your final UI.
The beauty of React is that when we change the state of a Component, rather than re-rendering the entire UI’s state, it only updates the Component’s state. This is what we mean by Component isolation.
In this image we have broken down the React UI Components of the MyHypermedia Web App that you will be implementing.
In our stencil code, each React component is organized into a folder. Let us say we had a
Donut
component. Then it would be a folder calledDonut
which contains the following files:useState
oruseEffect
is factored into a utils file.import { oreo } from '../../../../../../../donuts/chocolate/oreo'
)Verify that your frontend loads
First we should add the
.env
to yourclient
folder so that ESLint works as we expect it to! It should look as follows:Before we start writing frontend code, it’s important to check that your client runs as expected.
TODO:
Run the following
yarn
scripts to install the relevant dependencies and start the React services:cd client
yarn install && yarn start
To verify that your React service is running, if you go to
https://localhost:3000
in your browser then you should see the client web app that we have builtMyHypermedia
. If the web app does not load, please shout “TAaaa!!” (in Slack or in CIT219)Step 1:
createNode
in frontendNodeGateway.ts
We want to easily send and retrieve data from our database using our frontend app. Users will interact with data through buttons, forms etc. and we need a way to retrieve data. For that we will use a frontend
NodeGateway
to make HTTP requests.We use Axios to make our HTTP requests, they are all contained in
requests.ts
. ForcreateNode
we want to use a POST method since we are sending data from the client to the server!Now we need to define the arguments that we are passing into the post request:
url
:When you are running it locally the
baseEndpoint
variablehttps://localhost:5000
.Since this
NodeGateway
only sends HTTP requests to/node...
we use theservicePath
variable to define the permanent prefix attached to the route.Thus, our url should be:
baseEndpoint + servicePath + '/create'
body
:The body of the POST HTTP request should be the INode object that we are creating.
Finally, we arrive at the final method, which returns a
IServiceResponse
Promise.We can now access that from anywhere in our
client
to make an API request to create a node!You can look into the
createNodeFromModal
method increateNodeUtils.ts
to see an example!Step 2: ReactHooks & opening the CreateNodeModal
Before we do this step, we will introduce two very important hooks in React!
We will be using Functional Components with React Hooks in this course as opposed to the traditional Class Components in React. We do this because Functional Component requires much less boilerplate code, and is now the new industry standard.
React Hooks
First, let’s talk about React Hooks. If you are not familiar with it, the name really doesn’t explain anything at all, but we hope you will gradually like it as you get to know it more.
You can identify a React Hook by its names - they usually have
use
as a prefix to their name. Essentially, React Hooks deal with manipulating the state, or the value, of the variable. With hooks, React will automatically rerender (update) the components that contain this variable. Some of the really popular React Hooks includeuseState
anduseEffect
, and we will introduce them in this lab.useState
useState
is the most common hook, and it deals with the creating reactive variables - whenever reactive variables get updated, React will automatically rerender the component with the updated value. Imagine we want to have ahitCounter
variable that represents the number of button clicks. We can declare it with theuseState
hook:The first word,
const
, is the reserved keyword for declaring a new variable. However, we are actually declaring 2 things here: ahitCounter
variable of typenumber
, and a functionsetHitCounter
that is used to mutate the value ofhitCounter
. On the right side of the equal sign, we are calling theuseState
hook with the parameter0
- that indicates that thehitCounter
will be initialized at the value of0
.So we’ve used
useState
, is that it? Let’s see how we can use these 2 variables to achieve our goal - building a hit counter!Can you visualize what it would look like? If rendered in browser, there would probably be a
Count: 0
along with a button that saysHit Me!
. Let’s observe what is written inside the<button>
.We see that there is an
onClick
event handler written inside the<button>
div. Whatever is written inside theonClick
will be called when user clicks the button. In this case, we are callingsetHitCounter
with the parameterhitCounter + 1
to incremenethitCounter
everytime this button is pressed.hitCounter
’s current value is0
. With some math knowledge, we know that we are effectively putting1
as an argument into thesetHitCounter
; not surprisingly, React will now updatehitCounter
’s value to1
, and the DOM will showCount: 1
. If we keep clicking the button, this process will be repeated, and we can see thehitCounter
value to steadily increase.To recap,
useState
instantiates a variable with a default value, and React provides us with a mutator to change the variable’s value. Every update to variable will cause a rerender of the component.Note that the value of the
hitCounter
will be restored to 0 when we refresh the page. That is the anticipated behavior. If we want to make the data persistent through page refresh, we can use a database, or utilize the browser’s local storage.useEffect
Now, let’s learn another simple hook -
useEffect
. We can treat this hook as anif changed
statement. Let’s see what theuseEffect
hook looks like.For readability, we formatted the
useEffect
into one-line and commented on each part. AuseEffect
hook takes in 2 parameters, the first one being a callback function, and the second parameter is a dependency array.Callback Function
A callback function is simply a function that’s passed as an argument to another function!
Dependency Array
Dependency array is a very important concept to grasp. Essentially, it is the
if
part of theuseEffect
hook. We can put variables of different types into the dependency array, and whenever any variable in the dependency array gets updated, the callback function will be executed. For example, in the code snippet above, wheneverhitCounter
is updated, the callback function will print to the console. You can put as many variables as you want into the dependency array, but make sure to put in the right ones and avoid endless loops!There are 2 special cases for the dependency array:
useEffect(() => { console.log("hello!) }, [])
, it means the callback function will only be called once on component creation. When we want to make an API call when the page / componant loads, this mode comes in handy.useEffect(() => { console.log("change!")})
, the callback function will be executed whenever the component is rerendered.With this knowlegde, what does this snippet of code to do?
Whenever the button is clicked, the value of the
hitCounter
will be changed, triggering a useEffect call. The useEffect will print a string to console.Basic Rules of React Hook
if
conditions, or nested functions.Now that you have “mastered” React ;) let us move back to the codebase and implement the
create
button to open the modal. We use a UI package called Chakra to render our createNodeModal.Navigate to
MainView.tsx
, this is where we render the components and handle logic to load all of the components and the nodes from our database. We have divided the file into 6 sections, they are as follows:Step 1: Notice the following useState hook in [1]:
We use a state variable called
createNodeModalIsOpen
to keep track of state of theCreateNodeModal
.If the
createNodeModalIsOpen
variable changes, our MainView component will know to re-render the components that depend on thecreateNodeModalIsOpen
, but how do we change the variable? For that we need to use thesetCreateNodeModalIsOpen
function from our useState hook which updates the variable!Step 2: Go to [6], since the createNode button is located in the
Header
component we need theHeader
component to be able to access and call thesetCreateNodeModalIsOpen
function. How do we do this? We pass it in to the componant a aprop
(Note:props
stands for “properties”) to theHeader
component. You can even pass functions in as props!We’ll go into more detail on components and props in Step 3!
We should update the
onCreateButtonClick
prop in theHeader
component so that it looks as follows:Now our
Header
component takes in the method that updates our variable. It knows what to do when the create button is clicked! Try it out, and try creating a node with the CreateNodeModal!Congratulations! You’ve successfully implemented Create Node, from the backend all the way to the frontend!
Step 3: Implementing ImageContent.tsx
Now that we are able to create a node, what about viewing it? Sure we have an INode object with fields, but how useful is that? We need to take the INode oject and turn it into something useful.
This is what our
Boston Cream Donut
currently looks like, not so delicious:To start doing this, navigate to
ImageContent.tsx
.Here, we see the basic structure of a React component: it’s just a function! It takes an input
props
that is an object of shapeIContentProps
, and it outputs what it should render (for now,null
).IContentProps
is actually defined in a different file; you can Cmd+Click (Mac) or Ctrl+Click (Windows) on almost any expression in VSCode to see where it’s defined or used. A lot of people find this extremely useful; try it out!You can see that
IContentProps
has a field calledcontent
of typestring
. For an image node, this string is the image url.A common pattern when programming in React is to get the properties you need from a
props
object. Here are two ways to get thecontent
field from ourprops
:Once you have your
content
stored in aconst
, you will want to render an HTML image. You can do this by returning an<img />
HTML tag with thesrc
set to our image url (which is ourcontent
!).If you haven’t worked with React or JSX before, it might surprise you to see HTML in a Javascript function; essentially, we use
(...)
to denote HTML elements, and we use{...}
to denote Javascript when we are within an HTML element.For example:
Did you notice that
<img>
tag can be written in 2 formats?In JSX, which React uses, you can either use a traditional
<img src='...'></img>
to render an image, or to use a self-closing tag that looks like<img src='...' />
. This also applies to other HTML tags as well. They are equivalent expressions and either format is accepted.When you are done with your
ImageContent
component, you should see an image when you click on an image node. Ask a TA if you get stuck!Step 4: Implementing TextContent.tsx
Now we can implement our text content component in
TextContent.tsx
. The content of text nodes is simpler to render: it’s just astring
! You can render a Javascriptstring
by wrapping it in curly braces and putting it in an HTML tag (like adiv
).For example:
Once you render your text content, you should be see the content text when you click on a text node. Now, you’ll get some practice styling react components using
.scss
files!Styling with SCSS (.scss)
Throughout this class we will be using
.scss
files instead of .css files.SCSS files allows us to write SASS, which is an extension of CSS (Cascading Style Sheets) that adds functionality like variables and nesting. Because of these additional features, SCSS files are often used instead of CSS files in industry.
Note: While there are other alternatives for styling your React components, we’ll be sticking with SCSS for this project. We will also give you the option to use other CSS styling methods such as
tailwind
andemotion
later on in this class - or now if you are already familiar with them!In the same directory, go to
TextContent.scss
. Define a SCSS class for your text content – the syntax is like this (though the class can be named anything):Now, hook up this SCSS class to your React component by adding a
className
string to the HTML element you are rendering. You can do this like so:Ok, you’re now ready to start styling! Some good places to start are
font-size
,font-weight
,color
, andpadding
. You can add styles like so:Change these values and add more styles (See https://www.w3schools.com/html/html_styles.asp for some more examples, or do a quick Google search!) until you are happy with how your text is rendered.
Step 5: Rendering ImageContent.tsx and TextContent.tsx
Now that you have implemented
TextContent.tsx
andImageContent.tsx
, we need ourNodeView.tsx
to render either aTextContent
orImageContent
component depending on the type of node selected.In
client/src/components/NodeView/NodeView.tsx
we see this snippet of code returns what node content to render:Navigate to the implementation of
renderContent
bycmd + click
on Mac andctrl + click
on Windows.Implement the two
TODO
s in this file (nodeViewUtils.tsx
). Pay attention to what propsImageContent
andTextContent
require!Once done, your frontend should look like this!
Yay! Now you can render the content of
text
andimage
nodes from our Mongo database!Congrats! You’re done with the lab; see a TA to get checked off or attend TA Hours before the next lab section (September 23rd 3:00pm ET)