Data is the new oil - Clive Humbly
Everywhere around us is data waiting to be collected and utilized. In recent years we've seen the rise of applications and services that exist to quantify concepts that were previously hard to capture. FitBit, Apple Health, and Woop are all $1 billion dollar services to offer tracking statistics about how we live our lives. The LifeTracker app you'll be building will do exactly that - track your life by quantifying your activity.
This application will be built using the battle-tested PERN stack - PostgreSQL, Express, React, and Node.
Building this application you will accomplish the following:
- Develop a full-fledged authentication system using PostgreSQL and Bcrypt
- Provide users with an Express API they can interact with to store user-related activity
- Construct multiple Models that implement the core business logic associated with tracking users' lives
- Write SQL queries that aggregate user statistics and provide summary overviews about their activity
- Design a React frontend that interacts with the API using an API service class
- Build multiple pages and forms that communicate with the server using HTTP requests
- Store user-authenticated JWT tokens in the browser's local storage for persisted authentication
- Employ useEffect and useState hooks to manage application state on the frontend
This assignment asks SITE interns to accomplish the following:
For the React frontend, Site interns will be building a number of components. Make sure to keep the same component structure as the starter code, as that is how the tests will know where to find each component.
In general, the components directory should resemble:
│
└── components
└── App
│
├── Landing
│ └── Landing.jsx
│ └── Landing.css
... other components
├── Navbar
│ └── Navbar.jsx
│ └── Navbar.css
├── NutritionPage
│ └── NutritionPage.jsx
│ └── NutritionPage.css
... other components
NOTE: Each components should be exported from the file as the
default
export!
The components in the App.jsx
file should render the following components (along with any others that might be needed):
│
└── App
│
├── Navbar (all routes)
│ ├── Logo
│ └── NavLinks
│
├── LandingPage (the `/` route)
├── LoginPage (the `/login` route)
│ └── LoginForm
├── RegistrationPage (the `/register` route)
│ └── RegistrationForm
│
├── ActivityPage (the `/activity` route)
│ ├── SummaryStat
│ ├── SummaryStat
│ ├── ... as many as needed
│ └── SummaryStat
│
├── NutritionPage (the `/nutrition` route, which renders more nested routes)
│ ├── NutritionOverview (the main component rendered at the `/nutrition` route)
│ │ └── NutritionFeed
│ │ ├── NutritionCard
│ │ ├── NutritionCard
│ │ ├── ... as many as needed
│ │ ├── NutritionCard
│ │ └── NutritionCard
│ │
│ ├── NutritionNew (rendered at the `/nutrition/create` route)
│ │ └── NutritionForm
│ │
│ ├── NutritionDetail (rendered at the `/nutrition/id/:nutritionId` route)
│ │ └── NutritionCard
│ │ └── NotFound (only if no `nutrition` with that `nutritionId` exists)
│ │
│ └── NotFound (all other routes that don't match any of the nested routes)
│
└── NotFound (all other routes that don't match)
IMPORTANT: NOTE - Make sure that each component can render without crashing, even when none of the required props are provided! Components will be rigorously tested using only a few props. So, to ensure that all the tests pass, either add default props to components where needed, or check that certain props exist before rendering JSX that depends on them.
-
To build out the front-end, start with the
App.jsx
component:-
App.jsx
- Should be wrapped by an element with the
className
ofapp
- The core App component that contains the routes for the app wrapped in Context providers
- Renders the
Navbar
component on every route - Renders a
BrowserRouter
component that contains aRoutes
component with the following routes:-
/
- Should render theLanding.jsx
component -
/login
- Should render theLoginPage.jsx
component -
/register
- Should render theRegistrationPage.jsx
component -
/activity
- Should render theActivityPage.jsx
component (only if the user is logged in, otherwise it renders theAccessForbidden.jsx
component) -
/nutrition/* - should render the
NutritionPage.jsxcomponent (only if the user is logged in, otherwise it renders the
AccessForbidden.jsx` component) -
*
- anything else should render theNotFound
component
-
- Should be wrapped by an element with the
-
To standarize API requests throughout the application, set up an
ApiClient
class- Start by creating a
constants.js
file at the root of the project- In it, export a few variables:
-
PRODUCTION_API_BASE_URL
- set to whatever url the production API is deployed at -
DEVELOPMENT_API_BASE_URL
- set to "http://localhost:3001" for development -
API_BASE_URL
- ifprocess.env.NODE_ENV
isproduction
, set this toPRODUCTION_API_BASE_URL
, otherwise set it toDEVELOPMENT_API_BASE_URL
- Next, create a
services
directory at the root of the project - Inside that directory, touch an
apiClient.js
file - In that file, import the
axios
package and theAPI_BASE_URL
constant from theconstants.js
file - Define a new class in that file called
ApiClient
.- Give it a constructor function that accepts a single parameter -
remoteHostUrl
. The constructor should attach theremoteHostUrl
parameter to a new instance withthis.remoteHostUrl = remoteHostUrl
. It should also setthis.token = null
. - Export default a new instance of the
ApiClient
class - Add an additional method called
setToken
that accepts a single parameter -token
and attaches it to the instance. - Create a utility method called
request
that usesaxios
to issue HTTP requests - Add a
login
method that uses therequest
method to send an HTTP request to theauth/login
endpoint - Add a
signup
method that uses therequest
method to send an HTTP request to theauth/register
endpoint - Add a
fetchUserFromToken
method that uses therequest
method to send an HTTP request to theauth/me
endpoint - Add as many other methods as needed when making API requests
- Give it a constructor function that accepts a single parameter -
- Start by creating a
-
Create an
auth
context:- First, create a
contexts
directory at the root of the project - Inside it, touch the
contexts/auth.jsx
file - In that file, define a new
AuthContext
withReact.createContext
- Use that context to create an
AuthContextProvider
component- The Provider component should create state variables and updaters needed for
user
,initialized
,isProcessing
, anderror
. - It should have a
React.useEffect
hook that fires when the component is mounted to the screen- That hook should check to see if a JWT token exists in local storage under the
lifetracker_token
key- If it does:
- It should add that token to
ApiClient
class with thesetToken
method - Then, it should set the
isProcessing
state variable totrue
and theerror
state variable tonull
- Next, it should send a
GET
request to the/auth/me
endpoint- If it fails, it should set the
error
prop to a valid error message - If it is successful...
- It should set the
user
state variable with theuser
returned in the response - It should set the
error
state variable tonull
- It should set the
- If it fails, it should set the
- Regardless, it should set the
isProcessing
state variable tofalse
and theinitialized
state variable totrue
- It should add that token to
- The user returned from that request should be stored in state. This will ensure that users stay logged in even if they refresh the page.
- If it does:
- That hook should check to see if a JWT token exists in local storage under the
- It should also define handler functions for:
-
loginUser
- should make a request to log the user in -
signupUser
- should make a request to sign the user up -
fetchUserFromToken
- should make a request to the/auth/me
route to get the user's info -
logoutUser
- this function should remove thelifetracker_token
from local storage and refresh the page so that all user data is reset
-
- Make sure to set all the state variables as the
value
prop passed to theAuthContext.Provider
component
- The Provider component should create state variables and updaters needed for
- Create and export a
useAuthContext
hook that calls theReact.useContext
hook with the newly createdAuthContext
and returns it. - In
App.jsx
file create anAppContainer
component that wraps theApp
component with theAuthContextProvider
component (which should still be nested inside theBrowserRouter
component fromreact-router-dom
). Export theAppContainer
component by default instead of theApp
- First, create a
-
The
Loading.jsx
component- Should render JSX that is wrapped by an element with the
className
ofloading
- Should render an element with the
className
ofloading-message
that contains the text"Loading"
- Should render JSX that is wrapped by an element with the
-
The
Navbar.jsx
component- Should render JSX that is wrapped by a
nav
element with theclassName
ofnavbar
- Should render the app's logo:
- It should be an element with the
className
oflogo
. - Inside that element should be a
Link
component fromreact-router-dom
that navigates the user to the/
route when clicked. - Inside that
Link
component should be the application's logo (text or image)
- It should be an element with the
- Should render the
NavLinks.jsx
component with links to each of the resources and the/activity
route
- Should render JSX that is wrapped by a
-
The
NavLinks.jsx
component:- Should render JSX that is wrapped by an element with a
className
ofnav-links
- Should render a
Link
element fromreact-router-dom
for:- The
/activity
route. It should have a label ofActivity
. - The
/nutrition
route. It should have a label ofNutrition
. - A route for any other resource page
- The
- If a valid user is logged in:
- It should render an element with the
className
oflogout-button
that calls thelogoutUser
function when clicked.- That function should remove the
lifetracker_token
from local storage and refresh the page so that all user data is reset.
- That function should remove the
- It should render an element with the
- If no valid user is logged in:
- It should render a
Link
element that redirects to the/login
route with the labelLogin
- It should render a
Link
element that redirects to the/register
route with the labelSign Up
- It should render a
- Should render JSX that is wrapped by an element with a
-
The
LoginForm.jsx
component:- Should render JSX that is wrapped by an element with the
className
oflogin-form
- Should render an input element for the following fields:
-
email
-
password
-
- Each
input
element in the form should have aclassName
ofform-input
and should have the following props set:-
name
- thename
of theinput
field being rendered (email
,password
) -
type
- the type of theinput
element (text
,email
,number
, etc) -
value
- the current value of theinput
element -
onChange
- theonChange
handler function
-
- The component should validate the
email
field:- If the user has entered text into the
email
field and it doesn't contain an@
symbol, then an error message should be displayed in an element with theclassName
oferror
indicating that the entry is not a valid email.
- If the user has entered text into the
- The component should gracefully handle errors:
- If the user has attempted to login and gotten a
401
error, then an error message should be displayed in an element with theclassName
oferror
indicating that theemail
andpassword
combination is incorrect. - If the user has attempted to login and gotten a
400
or422
error, then an error message should be displayed in an element with theclassName
oferror
indicating what went wrong.
- If the user has attempted to login and gotten a
- There should be a
button
element with theclassName
ofsubmit-login
:- It should contain the text
"Login"
- When clicked, it should call the
loginUser
function
- It should contain the text
- Should render JSX that is wrapped by an element with the
-
The
LoginPage.jsx
component:- Should render JSX that is wrapped by an element with the
className
oflogin-page
- Using either a custom hook, context, or manually set state, this component should check to see if a user is already logged in
- If the user is already logged in, it should redirect them to the
/activity
page - If no user is authenticated, it should render the
LoginForm.jsx
component and pass it any props it needs
- If the user is already logged in, it should redirect them to the
- Should render JSX that is wrapped by an element with the
-
The
RegistrationForm.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofregistration-form
- Should render an input element for the following fields:
-
email
-
username
-
firstName
-
lastName
-
password
-
passwordConfirm
-
- Each
input
element in the form should have aclassName
ofform-input
and should have the following props set:-
name
- thename
of theinput
field being rendered (email
,username
,firstName
,lastName
,password
,passwordConfirm
) -
type
- the type of theinput
element (text
,email
,number
, etc) -
value
- the current value of theinput
element -
onChange
- theonChange
handler function
-
- The component should validate the
email
field:- If the user has entered text into the
email
field and it doesn't contain an@
symbol, then an error message should be displayed in an element with theclassName
oferror
indicating that the entry is not a valid email.
- If the user has entered text into the
- The component should validate the
password
andpasswordConfirm
fields:- If the user has entered text into the
password
andpasswordConfirm
fields and they don't match, then a message should be displayed in an element with theclassName
oferror
with a message that contains the text:passwords don't match
- If the user has entered text into the
- The component should gracefully handle errors:
- If the user has attempted to login and gotten a
401
error, then theerrors
object should contain aform
property that contains a message indicating that theemail
andpassword
combination is incorrect. - If the user has attempted to login and gotten a
400
or422
error, then theerrors
object should contain aform
property that contains a message indicating what went wrong.
- If the user has attempted to login and gotten a
- There should be a
button
element with theclassName
ofsubmit-registration
:- It should contain the text
"Create Account"
- When clicked, it should call the
signupUser
function
- It should contain the text
- Should render JSX that is wrapped by an element with the
-
The
RegistrationPage.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofregistration-page
- Using either a custom hook, context, or manually handled state, this component should check to see if a user is already logged in
- If the user is already logged in, it should redirect them to the
/activity
page - If no user is authenticated, it should render the
RegistrationForm.jsx
component and pass it any props it needs
- If the user is already logged in, it should redirect them to the
- Should render JSX that is wrapped by an element with the
-
The
LandingPage.jsx
component:- Should render JSX that is wrapped by an element with the
className
oflanding-page
- Should render an element with the
className
ofhero
- Inside it, display a large hero image using an
img
element with theclassName
ofhero-img
- Render a brief blurb on what this application is about inside an element with the
className
ofcta
- Inside it, display a large hero image using an
- Should allow unauthenticated access
- Should render JSX that is wrapped by an element with the
-
The
activity
context- Create a file in the
contexts directory -
/contexts/activity.jsx` - In that file, define a new
ActivityContext
withReact.createContext
- Use that context to create an
ActivityContextProvider
component- The
ActivityContextProvider
component should create state variables and updaters needed foractivity
,initialized
,isLoading
, anderror
. - It should call the
useAuthContext
hook and check to see if a valid user is logged in. - It should have a
React.useEffect
hook that fires when the component is mounted to the screen- That hook should check to see if a user is logged in.
- If a user is logged in...
- Set the
isLoading
state variable totrue
and theerror
state variable tonull
- Then, it should make a
GET
request to the/activity
endpoint- If there is an error with the request, it should set a message as the
error
state variable - If all goes well...
- It should set the data as the
activity
state variable - It should set the
error
state variable tonull
- It should set the data as the
- If there is an error with the request, it should set a message as the
- Regardless, at the end, set the
isLoading
state variable tofalse
and theinitialized
state variable totrue
- Set the
- Make sure to pass an object containing all the state variables to the
value
prop of theActivityContext.Provider
component
- The
- Create and export a
useActivityContext
hook that calls theReact.useContext
hook with the newly createdActivityContext
and returns it. - In the
App.jsx
file, nest theActivityContextProvider
inside theAuthContextProvider
.
- Create a file in the
-
The
ActivityPage.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofactivity-page
- It should call the
useActivityContext
hook and extract all the necessary data from it. - If the
isProcessing
flag istrue
, it should render theLoading.jsx
component - If the
isProcessing
flag isfalse
, it should render theActivityFeed.jsx
component and pass it the appropriate props
- Should render JSX that is wrapped by an element with the
-
The
ActivityFeed.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofactivity-feed
- Should accept at least the following props:
-
totalCaloriesPerDay
- an array of items containing summary data about the total calories consumed per day -
avgCaloriesPerCategory
- an array of items containing summary data about the average calories consumed per category - Any other
-
- Inside an element with the
className
ofper-category
, it should:- Render the text:
"Average Calories Per Category
inside anh4
element - Take the first
6
or less items in theavgCaloriesPerCategory
array and render aSummaryStat.jsx
component for each item.- It should pass the calories rounded down to one decimal place as the
stat
prop - It should pass the string of
calories
as thelabel
prop - It should pass the
category
as thesubstat
prop
- It should pass the calories rounded down to one decimal place as the
- Render the text:
- Inside an element with the
className
ofper-day
, it should:- Render the text:
"Total Calories Per Day
inside anh4
element - For each item in the
totalCaloriesPerDay
array, it should render aSummaryStat.jsx
component.- It should pass the calories rounded down to the nearest whole number as the
stat
prop - It should pass the string of
calories
as thelabel
prop - It should pass the
date
in the formatdd/mm/yyyy
- example:07/02/2022
- as thesubstat
prop
- It should pass the calories rounded down to the nearest whole number as the
- Render the text:
- Should render JSX that is wrapped by an element with the
-
The
SummaryStat.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofsummary-stat
- Should accept at least the following props:
-
stat
- the primary statistic to display -
label
- the unit label assigned to the statistic -
substat
- a secondary statistic related to the primary statistic
-
- It should render the
stat
prop inside an element with theclassName
ofprimary-statistic
- It should render the
label
prop inside an element with theclassName
ofstat-label
- It should render the
substat
prop inside an element with theclassName
ofsecondary-statistic
- Should render JSX that is wrapped by an element with the
-
The
nutrition
context- Create a file in the
contexts directory -
/contexts/nutrition.jsx` - In that file, define a new
NutritionContext
withReact.createContext
- Use that context to create a
NutritionContextProvider
component- The
NutritionContextProvider
component should create state variables and updaters needed fornutritions
,initialized
,isLoading
, anderror
. - It should call the
useAuthContext
hook and check to see if a valid user is logged in. - It should have a
React.useEffect
hook that fires when the component is mounted to the screen- That hook should check to see if a user is logged in.
- If a user is logged in...
- Set the
isLoading
state variable totrue
- Then, it should make a
GET
request to the/nutritions
endpoint- If there is an error with the request, it should set a message as the
error
state variable - If all goes well:
- It should set the data as the
nutritions
state variable
- It should set the data as the
- If there is an error with the request, it should set a message as the
- Regardless, at the end, set the
isLoading
state variable tofalse
and theinitialized
state variable totrue
- Set the
- Make sure to pass an object containing all the state variables to the
value
prop of theNutritionContext.Provider
component
- The
- Create and export a
useNutritionContext
hook that calls theReact.useContext
hook with the newly createdNutritionContext
and returns it.
- Create a file in the
-
The
NutritionPage.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-page
- Should render a nested
Routes
component fromreact-router-dom
.- There should be multiple
Route
components:- The
/nutrition
route should render theNutritionOverview.jsx
component - The
/nutrition/create
route should render theNutritionNew.jsx
component - The
/nutrition/id/:nutritionId
should render theNutritionDetail.jsx
component - Any other route should render the
NotFound
component
- The
- There should be multiple
- Should render JSX that is wrapped by an element with the
-
The
NutritionOverview.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-overview
- It should call the
useNutritionContext
hook and extract all the necessary data from it.- If the
error
state variable has a valid string in it, it should render theerror
message inside an element with theclassName
oferror
- If the
isLoading
boolean istrue
, it should render theLoading.jsx
component - If the
isLoading
boolean isfalse
, it should render theNutritionFeed.jsx
component and pass it the appropriate props
- If the
- Near the top of the component, it should render a
Link
component that directs to the/nutrition/create
route and contains the text:"Record Nutrition"
- Should render JSX that is wrapped by an element with the
-
The
NutritionFeed.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-feed
- It should receive at least the following props:
-
nutritions
- an array ofnutrition
items
-
- If the
nutritions
array has no items in it, it should render an empty message that saysNothing here yet
inside an element with theclassName
ofempty-message
- If the
nutritions
array does have items in it:- For each item in the
nutritions
array, it should render aNutritionCard.jsx
component
- For each item in the
- Should render JSX that is wrapped by an element with the
-
The
NutritionNew.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-new
- Should render the
NutritionForm.jsx
component and pass it the appropriate props
- Should render JSX that is wrapped by an element with the
-
The
NutritionForm.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-form
- Should render an input element for the following fields:
-
name
- name of the nutrition item (defaults to an empty string) -
calories
- number of calories in the nutrition item (defaults to 1) -
imageUrl
- theurl
of an image to show for this nutrition item (defaults to an empty string) -
category
- the category that this nutrition item belongs to, like fruit, meat, soda, snack, nuts, etc. (defaults to an empty string)
-
- Each
input
element in the form should have aclassName
ofform-input
and should have the following props set:-
name
- thename
of theinput
field being rendered (name
,calories
,imageUrl
,category
) -
type
- the type of theinput
element (text
,email
,number
, etc) -
value
- the current value of theinput
element -
onChange
- theonChange
handler function
-
- The component should gracefully handle errors:
- If any of the required fields are left blank, there should be an error message inside of an element with the
className
oferror
indicating which fields are required. - If the user has attempted to create a nutrition entry and gotten a
400
or422
error, then that message should be displayed inside an element with theclassName
oferror
- If any of the required fields are left blank, there should be an error message inside of an element with the
- There should be a
button
element with theclassName
ofsubmit-nutrition
:- It should contain the text
"Save"
- When clicked, it should call a function that creates a new nutrition entry
- It should contain the text
- After the form has been succesfully submitted:
- Ensure that the new nutrition entry is stored in the
nutrition
context'snutritions
array and is displayed in theNutritionFeed.jsx
component - Refetch the
activity
data so that new summary stats will be calculated
- Ensure that the new nutrition entry is stored in the
- Should render JSX that is wrapped by an element with the
-
The
NutritionDetail.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-detail
- It should leverage the
useParams
hook fromreact-router-dom
to extract thenutritionId
param from the url - When the component is mounted to the screen...
- It should make a
GET
request to the/nutrition/:nutritionId
endpoint with theaxios.get
method. - The
:nutritionId
part of the request should be replaced with thenutritionId
pulled from the url. - When the initial request is loading, it should render an
h1
element with theclassName
ofloading
and contain the text"Loading..."
- It should store the
nutrition
received by the request in state and then render aNutritionCard.jsx
component for that nutrition. - If no
nutrition
is found with thatid
, it should render theNotFound.jsx
component
- It should make a
- Should render JSX that is wrapped by an element with the
-
The
NutritionCard.jsx
component:- Should render JSX that is wrapped by an element with the
className
ofnutrition-card
- Should accept at least the following props:
-
nutrition
- should be a nutrition entry object containing the following attributes:-
imageUrl
- (not required) -
name
- (required) -
calories
- (required) -
category
- (required) -
createdAt
- (required)
-
-
- It should render the
name
of thenutrition
entry inside an element with theclassName
ofnutrition-name
- If the
nutrition
entry has a validimageUrl
attribute, it should render animg
element with theclassName
ofnutrition-image
and use thatimageUrl
as itssrc
- It should render the
calories
attribute of thenutrition
entry inside an element with theclassName
ofnutrition-calories
- It should render the
category
attribute of thenutrition
entry inside an element with theclassName
ofnutrition-category
- It should render the
createdAt
attribute of thenutrition
entry in the formatdd/mm/yyyy
- example:07/02/2022
- inside an element with theclassName
ofnutrition-date
.
- Should render JSX that is wrapped by an element with the
-
DO THE SAME FOR ANY OTHER RESOURCE THAT IS IN THE APPLICATION
- Choose whatever resources you want!
-
The
ProtectedRoute.jsx
component:- Create a
ProtectedRoute.jsx
component that uses theuseAuthContext
hook to get access to theinitialized
anduser
variables. - It should accept a component as the
element
prop and render that component. - If the application isn't currently loading and no user is found, it should render the
LoginPage.jsx
component instead of rendering the route the user intended to go to. This way, we can ensure that only authenticated users can access the provided component. - Any unauthenticated user should be shown the
LoginPage.jsx
component with a message indicating that they need to authenticate first - Update the
LoginPage.jsx
component so that it accepts amessage
prop that is displayed in the login form - if it exists. - Make sure to protect the entire
ActivityPage
component route and theNutritionPage
component route (along with any other private resource pages). Don't protect theLandingPage
component or theLoginPage
andRegistrationPage
components, as they should be public.
- Create a
-
Note that when developing the backend, SITE interns will be asked to write tests for each API feature. However, the test suite will NOT be checking to see if actual tests have been written. These tests are only there to guide development. The features themselves are what will be evaluated.
Here are the pieces of functionality that should be built out for the backend:
- Project setup
- First things first, bootstrap the Express application with some essential files and starter code
- Create a
.gitignore
file, anapp.js
file, anapp.test.js
file, and aserver.js
file - Make sure
node_modules
are added to the.gitignore
file. - Add dependencies for
express@next
,morgan
,cors
, andnodemon
- Install new dependencies for
bcrypt
,jsonwebtoken
,colors
,dotenv
,pg
- Commit all work to
git
- Add a
.env
file to the root of the repo and include the following environment variables-
PORT
(default to3001
) -
SECRET_KEY
(set to a long random string) -
BCRYPT_WORK_FACTOR
(set to13
) -
DATABASE_USER
-
DATABASE_PASS
-
DATABASE_HOST
-
DATABASE_PORT
-
DATABASE_NAME
- (set tolifetracker
) -
DATABASE_TEST_NAME
- (set tolifetracker_test
)
-
- Add a
config.test.js
file- Write tests that check to make sure that:
-
process.env.NODE_ENV
is set totest
when the test suite is run - There is an
IS_TESTING
variable that is exported, which should only be true ifprocess.env.NODE_ENV
is set totest
-
- Write tests to ensure that certain environment variables are exported from the
config.js
file and can be imported:-
PORT
-
SECRET_KEY
-
BCRYPT_WORK_FACTOR
-
IS_TESTING
-
- Write tests to ensure that a
getDatabaseUri
function is exported from theconfig.js
file- The
getDatabaseUri
function should:- Check to see if a valid
process.env.DATABASE_URL
environment variable exists, and return that if it does. - When
IS_TESTING
istrue
, thegetDatabaseUri
function should use the test database - Otherwise, it should combine the proper database environment variables into a database connection string if no
process.env.DATABASE_URL
environment variable exists
- Check to see if a valid
- The
- Write tests that check to make sure that:
- Add a
config.js
file- Use the
dotenv
package to parse the environment variables from the.env
file. - Export each of the environment variables from the
config.js
file until the tests pass - Write a
getDatabaseUri
function so that all the tests pass
- Use the
- Commit all work to
git
- The project should now be ready to go!
- PostgreSQL database
- Time bring in a PostgreSQL database client as the application's persistence layer
- Make sure the PostgreSQL server is running
- Create two files at the root of the project:
-
lifetracker-schema.sql
- This script should:
- Create a
users
table with the following columns:-
id
-
username
-
password
-
first_name
-
last_name
-
email
-
created_at
-
updated_at
-
- Create a
nutrition
table with the following columns:-
id
-
name
-
category
-
calories
-
image_url
-
user_id
-
created_at
-
- Any other tables that the application might depend on
- Create a
- This script should:
-
lifetracker.sql
- This script should:
- 1. Let the user know that they're about to delete the
lifetracker
db and prompt them to confirm that is what they want. - 2. Drop the
lifetracker
database and then create a newlifetracker
database, before connecting to thelifetracker
database. - 3. It should then run the
lifetracker-schema.sql
file. - Follow the exact same steps for
1
,2
, and3
, but with thelifetracker_test
database.
- 1. Let the user know that they're about to delete the
- This script should:
-
- Setup the database by running
psql -f lifetracker.sql
- Create a new file at the root of the project called
db.js
. In that file:- Import the
getDatabaseUri
function from theconfig.js
file. - Initialize a new postgres client with the
pg
package and connect to PostgreSQL using any necessary config variables. - Connect to postgres and log a message to the terminal on success or failure.
- Export the connected database client
- Import the
- Commit all work to
git
- A database client is now ready to be used!
- Server
- Build out a bare-bones Express server with a healthcheck route and an adequate middleware pipeline.
- Create a
utils
directory- In the
utils
directory, create anerrors.js
file. - Create error classes inside the file that will be used throughout the app.
- In the
- In the
app.test.js
file, write tests that:- Ensure that the Express application responds to
GET
requests to the/
route with a JSON object of{ "ping": "pong" }
- Check that middleware like
morgan
andcors
exist, along with the JSONbody-parser
middleware fromexpress
- Include an
afterAll
hook that callsawait db.end()
so that any open database connections close when all the tests are finished.
- Ensure that the Express application responds to
- Add code to the
app.js
andserver.js
file to get a simple server running along with responding toGET
requests to the/
route - Create error classes inside the
utils/errors.js
file. - Add
404
and generic error handler middlewares to theapp.js
file. - In the
server.js
file:- Import the Express app and the
config.js
file - Have the
app
listen on the port specified byconfig.PORT
.
- Import the Express app and the
- Commit all work to
git
- Test out the fancy new Express server by starting it up in a new terminal window!
- Common Test Configuration
- It would probably be helpful to create some common test functions that can be used throughout the application's testing suite.
- Create a new directory called
tests
- Now, touch a new file at
tests/common.js
- In that file:
- Import the
db
client - Create and export four functions:
-
commonBeforeAll
- Actions that should happen before any tests in a particular file run.
- This should include things like executing queries that delete all items from any tables in the test database that might have been added during testing
-
commonBeforeEach
- Actions that should happen before any single test in a particular file runs.
- This should include things like starting a database transaction
-
commonAfterEach
- Actions that should happen after any single test in a particular file runs.
- This should include things like rolling back any database actions before they're committed
-
commonAfterAll
- Actions that should occur after all tests in a particular file run.
- This should include things like ending any open database client connections
-
- Import the
- In that file:
- Commit all work to
git
- Authentication
- Go ahead and build out a full-fledged authentication flow using PostgreSQL,
bcrypt
, and JSON Web Tokens. For it all to work, we'll need aUser
model, asecurity
middleware, sometokens
utility functions, and the appropriateauth
routes. - Add new directories for
models
,routes
, andmiddleware
- The User model
- In the
models
directory, create two new files:models/user.js
andmodels/user.test.js
- The
User
model should have at least the following static methods:-
login
-
register
-
fetchUserByEmail
-
- The
- In the
models/user.test.js
file:- Test the
login
method. Write test cases for:- User can login successfully with proper credentials
- Unknown email throws
UnauthorizedError
- Invalid credentials throws
UnauthorizedError
- Test the
register
method. Write test cases for:- User can successfully register with proper credentials
- Registering with duplicate email throws
BadRequestError
- Registering with duplicate username throws
BadRequestError
- Registering with invalid email throws
BadRequestError
- Test the
fetchUserByEmail
method:. Write test cases for:- A valid email returns a user from the database
- Invalid emails are handled correctly
- It will probably be important to use the
beforeAll
,afterAll
,beforeEach
, andafterEach
hooks to add and delete users from the database before running the tests
- Test the
- In the
models/user.js
file:- Import the
bcrypt
package, thedb
client, and the appconfig
. - Implement the features outlined in the tests until they're all passing.
- Import the
- In the
- Commit all work to
git
- The tokens utility functions
- In the
utils
directory, create two new files:utils/tokens.js
andutils/tokens.test.js
- At the bare minimum, two functions will be needed:
- One that accepts a JSON payload as an argument and converts it into a JWT
- One that accepts a JWT as an argument, validates it, and returns the JSON payload encoded within - if it's valid
- At the bare minimum, two functions will be needed:
- In the
utils/tokens.test.js
file:- Write test cases for:
- Can create valid JWT tokens for user payloads
- Can extract a payload from a valid JWT with the correct secret
- No payload gets returned when invalid tokens are parsed
- Write test cases for:
- In the
utils/tokens.js
file:- Implement the features outlined in the tests until they're all passing
- In the
- Commit all work to
git
- The security middleware
- In the
middleware
directory, create two new files:middleware/security.js
andmiddleware/security.test.js
- One middleware will be responsible for extracting a user from a valid JWT in the request:
- Checking the
Authentication
header of each request for the existence of a JWT. - If one exists, it should extract the token, validate it, extract the encoded JSON payload, and attach it to the response's
locals
property
- Checking the
- One middleware will be responsible for ensuring that an authenticated user exists:
- Checking that a valid user exists on the response's
locals
property - If one does, the middleware should simply call next
- If no valid user exists, it should throw an
UnauthorizedError
- Checking that a valid user exists on the response's
- One middleware will be responsible for extracting a user from a valid JWT in the request:
- In the
middleware/security.test.js
file:- Test the
Authentication
header parsing middleware- Write test cases for:
- Extracts user from valid JWT in
Authentication
header - No user is stored when no valid JWT exists in the
Authentication
header - No user is stored when an invalid JWT is in the
Authentication
header
- Extracts user from valid JWT in
- Write test cases for:
- Test the middleware that ensures an authenticated user exists
- Write test cases for:
- Doesn't throw an error when a valid user is present
- Throws an
UnauthorizedError
when no valid user is present
- Write test cases for:
- Test the
- In the
middleware/security.js
file:- Implement the features outlined in the tests until they're all passing
- In the
app.js
file, add theAuthentication
header parsing middleware to the Express app's middleware pipeline
- In the
- Commit all work to
git
- The /auth routes
- In the
routes
directory, create two new files:routes/auth.js
androutes/auth.test.js
- A new Express router should be created. It should handle:
- A
GET
request to the/me
endpoint- It should send a JSON response back to the client with the user info like so:
{ "user": { "email": "[email protected]", ... } }
- It should send a JSON response back to the client with the user info like so:
- A
POST
request to the/login
endpoint- It should accept a request body with
email
andpassword
keys - It should send a JSON response back to the client with a new JWT and user info like so:
{ "token": "e2c2...", "user": { "email": "[email protected]", ... } }
- It should accept a request body with
- A
POST
request to the/register
endpoint- It should accept a request body with
email
,username
,firstName
,lastName
, andpassword
keys - It should send a JSON response back to the client with a
201
status code, along with a new JWT and user info like so:{ "token": "e2c2...", "user": { "email": "[email protected]", ... } }
- It should accept a request body with
- A
- It should be mounted at the
/auth
endpoint in theapp.js
file
- A new Express router should be created. It should handle:
- In the
routes/auth.test.js
file:- Test the
POST /auth/login
endpoint- Write test cases for:
- Allows user to register with valid credentials and responds with JSON containing a valid token and user in the "token" and "user" fields
- Throws
UnauthorizedError
when user doesn't exist in db - Throws
UnauthorizedError
when user provides wrong password - Throws
BadRequestError
when user doesn't provide password - Throws
BadRequestError
when user doesn't provide email
- Write test cases for:
- Test the
POST /auth/register
endpoint- Write test cases for:
- Allows user to login successfully with valid credentials and responds with a
201
status code, along with JSON containing a valid token and user in the "token" and "user" fields - Throws
BadRequestError
when user doesn't provide one of the required fields - Throws
BadRequestError
when user provides email that already exists - Throws
BadRequestError
when user provides username that already exists
- Allows user to login successfully with valid credentials and responds with a
- Write test cases for:
- Test the
POST /auth/me
endpoint- Write test cases for:
- Provides the user with their user info when a valid JWT is present in the
Authentication
header of the request - Throws an
UnauthorizedError
when no valid user is logged in
- Provides the user with their user info when a valid JWT is present in the
- Write test cases for:
- Test the
- In the
routes/auth.js
file:- Create a new Express router
- Implement the features outlined in the tests until they're all passing
- In the
app.js
file: - Mount the router at the
/auth
endpoint
- In the
- Commit all work to
git
- There should now be a full-fledged authentication system in place!
- Go ahead and build out a full-fledged authentication flow using PostgreSQL,
- Resources and Permissions
- Next, implement the functionality to allow users to save instances of things they've drank/eaten, so that they can track their own nutrition data! Also make sure users can only access the data that they themselves have created. No other user should be able to see any data owned by another user!
- The Nutrition model
- In the
models
directory, create two new files:models/nutrition.js
andmodels/nutrition.test.js
- The
Nutrition
model should have at least the following static methods:-
createNutrition
- Should insert a new nutrition instance into the database when values are supplied for all of the required fields:
"name"
,"category"
,"calories"
, and"image_url"
. Thequantity
field should default to1
. - The new nutrition instance should have its
user_id
field set to theid
of the authenticated user - Should throw a
BadRequestError
(400
status code) orUnprocessableEntityError
(422
status code) when any of those values are not supplied.
- Should insert a new nutrition instance into the database when values are supplied for all of the required fields:
-
fetchNutritionById
- When supplied with a valid
id
, fetches the a nutrition instance from the database that matches thatid
. - If no nutrition instance matches that
id
, throws aNotFoundError
(404
status code)
- When supplied with a valid
-
listNutritionForUser
- Should list all nutrition instances in the database that are owned by a particular user
-
- The
- In the
models/nutrition.test.js
file:- Test the
createNutrition
method. Write test cases for:- A user can create a nutrition instance when they supply the appropriate values
- The appropriate error is thrown when any of the provided errors are invalid
- The user that creates the nutrition instance now owns that nutrition instance
- Test the
fetchNutritionById
method. Write test cases for:- Fetches the nutrition instance that matches the supplied
id
- Throws a
NotFoundError
when no nutrition instances matches the suppliedid
- Fetches the nutrition instance that matches the supplied
- Test the
listNutritionForUser
method. Write test cases for:- Fetches all nutrition instances belonging to a particular user
- Doesn't include any nutrition instances belonging to a different user
- Returns an empty array if no nutrition instances are found in the database that belong to that user
- Test the
- In the
models/nutrition.js
file:- Implement the features outlined in the tests until they're all passing
- Commit all work to
git
- In the
- The permissions middleware
- In the
middleware
directory, create two new files:middleware/permissions.js
andmiddleware/permissions.test.js
- Though more functions will need to be added here as the number of resources grows, for now only 1 function needs to be created.
- The
authedUserOwnsNutrition
middleware function should:- Probably be called after the
requireAuthenticatedUser
security middleware in any route's middleware pipeline - Extract a parameter from the request endpoint that corresponds to the
id
of the nutrition instance - Query the database for that nutrition instance
- Check that it is owned by the authenticated user
- If it doesn't, it should throw a
ForbiddenError
(403
status code) - If the nutrition instance does belong to the authed user, it should attach it to the
locals
property of theresponse
as itsnutrition
property so that it doesn't need to be fetched again by the database (this isn't required, but is probably a good idea).
- If it doesn't, it should throw a
- Probably be called after the
- In the
middleware/permissions.test.js
file:- Test the
authedUserOwnsNutrition
middleware function- Write test cases for:
- Throws error if authed user doesn't own nutrition
- Throws
NotFoundError
ifid
of nutrition isn't found in database - Doesn't throw error if authed user is nutrition owner
- (OPTIONAL) Attaches the
nutrition
to thelocals
property of the response when the user owns the nutrition instance
- Write test cases for:
- Test the
- In the
middleware/permissions.js
file:- Implement the features outlined in the tests until they're all passing
- Commit all work to
git
- In the
- The /nutrition routes
- In the
routes
directory, create two new files:routes/nutrition.js
androutes/nutrition.test.js
- A new Express router should be created that will be mounted at the
/nutrition
endpoint. It should handle:-
GET
requests to the/
endpoint- It should send a JSON response back to the client with all of the user-owned nutrition instances in an array like so:
{ "nutritions": [...] }
- It should send a JSON response back to the client with all of the user-owned nutrition instances in an array like so:
-
POST
requests to the/
endpoint- It should accept a request body with one
nutrition
key containing an object with all the attributes of thenutrition
entry - It should send a JSON response back to the client with a
201
status code, and the newly created nutrition instance like so:{ "nutrition": { ... } }
- It should accept a request body with one
-
GET
requests to the/:nutritionId
endpoint- It should send a JSON response back to the client with the nutrition instance that matches the
:nutritionId
parameter like so:{ "nutrition": { ... } }
- It should send a JSON response back to the client with the nutrition instance that matches the
-
- A new Express router should be created that will be mounted at the
- In the
routes/nutrition.test.js
file:- Test the
GET /nutrition
endpoint- Write test cases for:
- Returns an array of all
nutrition
entries belonging to the user - Other user's entries aren't included in the
nutritions
array - Throws
UnauthorizedError
if no valid user is logged in
- Returns an array of all
- Write test cases for:
- Test the
POST /nutrition
endpoint- Write test cases for:
- Authenticated users can create a new
nutrition
entry when providing values for all the required fields - The new
nutrition
entry belongs to the user that created it - Throws a
BadRequestError
if any of the required fields are missing - Throws an
UnauthorizedError
if no valid user is logged in
- Authenticated users can create a new
- Write test cases for:
- Test the
GET /nutrition/:nutritionId
endpoint- Write test cases for:
- Nutrition owner can fetch a
nutrition
entry when providing a validid
- Throws a
403 ForbiddenError
if a user tries to access anutrition
instance that does not belong to them - Throws a
404 NotFoundError
when thenutritionId
does not match any nutrition in the database - Throws a
401 UnauthorizedError
if no valid user is logged in
- Nutrition owner can fetch a
- Write test cases for:
- Test the
- In the
routes/nutrition.js
file:- Implement the features outlined in the tests until they're all passing
- In the
- Commit all work to
git
- Additional Resources
- Create model and routes files for 1-2 additional resources that your app will track (sleep, exercise, steps, floors climbed, meditation, mood, heartrate, music practice, etc)
- Commit all work to
git
- Summary Statistics
- One of the last features of the API will be a model that calculates summary statistic on the different resources that users are tracking. This includes statistics like average calories per day, or max calories per category. To do that, we'll create a new
Activity
model and anactivity
route that will be used to populate the frontend. - The Activity model
- In the
models
directory, create two new files:models/Activity.js
andmodels/Activity.test.js
- The
Activity
model should have at least the following static methods:-
calculateDailyCaloriesSummaryStats
- Should execute a SQL query that calculates at least the total calories consumed per day (aliased as
totalCaloriesPerDay
), along with the day (aliased asdate
). - The query should return a row for each day containing the total calories consumed per day, and the average calric content per nutrition entry.
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
-
{ id: 1, user_id: 1, calories: 100, category: "candy", created_at: "12-22-2022" }
-
{ id: 2, user_id: 1, calories: 200, category: "drink", created_at: "12-22-2022" }
-
{ id: 3, user_id: 1, calories: 200, category: "fruit", created_at: "12-23-2022" }
-
{ id: 4, user_id: 1, calories: 400, category: "dairy", created_at: "12-23-2022" }
-
{ id: 5, user_id: 1, calories: 400, category: "drink", created_at: "12-23-2022" }
-
{ id: 6, user_id: 1, calories: 700, category: "fruit", created_at: "12-24-2022" }
-
{ id: 7, user_id: 1, calories: 100, category: "fruit", created_at: "12-24-2022" }
-
- The summary stats returned from the query should look like this:
-
{ date: "12-22-2022", totalCaloriesPerDay: 300 }
-
{ date: "12-23-2022", totalCaloriesPerDay: 1000 }
-
{ date: "12-24-2022", totalCaloriesPerDay: 800 }
-
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
- Should execute a SQL query that calculates at least the total calories consumed per day (aliased as
-
calculatePerCategoryCaloriesSummaryStats
- Should execute a SQL query that calculates at least the average calories consumed per category (aliased as
avgCaloriesPerCategory
and rounded down to one decimal place), along with the category (aliased ascategory
). - The query should return a row for each day containing the total calories consumed per day, and the average calric content per nutrition entry.
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
-
{ id: 1, user_id: 1, calories: 100, category: "candy", created_at: "12-22-2022" }
-
{ id: 2, user_id: 1, calories: 200, category: "drink", created_at: "12-22-2022" }
-
{ id: 3, user_id: 1, calories: 200, category: "fruit", created_at: "12-23-2022" }
-
{ id: 4, user_id: 1, calories: 400, category: "dairy", created_at: "12-23-2022" }
-
{ id: 5, user_id: 1, calories: 400, category: "drink", created_at: "12-23-2022" }
-
{ id: 6, user_id: 1, calories: 700, category: "fruit", created_at: "12-24-2022" }
-
{ id: 7, user_id: 1, calories: 100, category: "fruit", created_at: "12-24-2022" }
-
- The summary stats returned from the query should look like this:
-
{ category: "candy", avgCaloriesPerCategory: 100.0 }
-
{ category: "drink", avgCaloriesPerCategory: 300.0 }
-
{ category: "fruit", avgCaloriesPerCategory: 266.6 }
-
{ category: "dairy", avgCaloriesPerCategory: 400.0 }
-
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
- Should execute a SQL query that calculates at least the average calories consumed per category (aliased as
-
- The
- In the
models/Activity.test.js
file:- Test the
calculateDailyCaloriesSummaryStats
method. Write test cases for:- The
calculateDailyCaloriesSummaryStats
method correctly calculates summary statistics per day - Only uses the
nutrition
entries belonging to the user when calculating summary statistics - Returns an empty array when the user has no
nutrition
entries
- The
- Test the
calculatePerCategoryCaloriesSummaryStats
method. Write test cases for:- The
calculatePerCategoryCaloriesSummaryStats
method correctly calculates average calories per category summary statistics - Only uses the
nutrition
entries belonging to the user when calculating summary statistics - Returns an empty array when the user has no
nutrition
entries
- The
- Test the
- In the
models/Activity.js
file:- Implement the features outlined in the tests until they're all passing
- Commit all work to
git
- In the
- The /activity routes
- In the
routes
directory, create two new files:routes/activity.js
androutes/activity.test.js
- A new Express router should be created that will be mounted at the
/activity
endpoint. It should handle:-
GET
requests to the/
endpoint- It should send a JSON response back to the client with summary stats for each resource in the following format:
-
{ "nutrition": { "calories": { "perDay": [...], "perCategory": [...] }, ...anyOtherStats }, ...statsForOtherResources }
-
- It should send a JSON response back to the client with summary stats for each resource in the following format:
-
- A new Express router should be created that will be mounted at the
- In the
routes/activity.test.js
file:- Test the
GET /activity
endpoint- Write test cases for:
- Provides a JSON response containing arrays of summary stats for resources, attributes, and metrics
- Correctly calculates
totalCaloriesPerDay
for a user'snutrition
entries - Correctly calculates
avgCaloriesPerCategory
for a user'snutrition
entries - Only returns summary stats based on entries that the currently authenticated user owns
- Throws an
UnauthenticatedError
if no valid user is logged in
- Write test cases for:
- Test the
- In the
routes/activity.js
file:- Implement the features outlined in the tests until they're all passing
- In the
- Commit all work to
git
- One of the last features of the API will be a model that calculates summary statistic on the different resources that users are tracking. This includes statistics like average calories per day, or max calories per category. To do that, we'll create a new
This is a huge project and represents a complete application with user-owned resources. Accordingly, it can, should, and will test the limits of your capabilities. Do your best and don't be afraid to Google! Especially in regards to any SQL that might be tricky.