React Express Auth Template Overview
The purpose of this repository is to provide a template for full-stack applications that may be studied and built on top of. In this document, you will find information explaining how the components of this full-stack application interact with each other as well as specific implementation details for user authentication.
Table of Contents
Application Overview
This repository provides a foundation for any application you want to build that requires users. Using this template repository, you can add any additional features and designs that you desire! For example, you might create:
A social media application where users can create/manage groups, forums, posts, comments, likes, etc…
An event management application where users can create/manage events, RSVPs, etc...
A personal productivity application where users can create/manage journal entries, trackers, todo lists, etc…
A business application with multiple types of users (e.g. doctors and patients, buyers and sellers, etc...) and users can create/manage products, carts, appointments, etc…
The backend of this repository already includes migrations and seeds, a model, and endpoints to manage a users table with the following properties:

With this functionality, users can:
Register (create) a new user account with a username and password
Log in using their credentials.
View a list of all users
View a single user's page
Update their username (and only their own)
Log out
The frontend is built using React with very minimal styling. Below, you can see the user journey as they navigate through the frontend:

Here is a detailed breakdown of the user journey above and how the frontend interacts with various backend endpoints:
When a user visits the
"/"page, they see the Home page and navigation buttons to either a login page or a sign up page.Users can authenticate by visiting the
"/login"or"/sign-up"pages.Login requests are sent to the
POST /api/auth/loginendpoint.Sign up requests are sent to the
POST /api/auth/registerendpoint.Previously authenticated users will be automatically signed in via the
GET /api/auth/meendpoint and redirected to"/".
After logging in or signing up users are taken to the
"/users/:id"page to view their own profile. Since they are authorized to modify their own profile, they have the option to update their username and log out.Individual user data is provided via the
GET /api/users/:idendpoint.Username updates are provided via the
PATCH /api/users/:idendpoint which requires authorization.User log out functionality is provided via the
DELETE /api/auth/logoutendpoint.
Users can visit the
"/users"page to view all users in the application. Clicking on a user's name takes them to their profile page. Since they are not authorized, they can only view this user's profile.The list of all users is provided via the
GET /api/usersendpoint.
What you choose to build and how you choose to design this application is entirely up to you! Have fun and make something that changes the world!
Getting Started
Now that you understand what this app can do, let's jump in and get it running locally!
Create your repo
If you are working on a team, first make sure that your team has a new GitHub Organization for your project.
Select Use this template and select Create a new repository. Rename the repo and choose your GitHub organization as the owner. If you are working alone, just select your own account as the owner.
Clone your repo and open it in your editor of choice!
Getting to know the folder structure
In the root of this repository are the two sides of the application
frontend/— the front-end application code (React)server/— the back-end server application code (Node + Express)
Each of these sub-directories has its own package.json file with its own dependencies and scripts.
The root of the entire repo also has a package.json file. It has no dependencies but does include some scripts for quickly getting the project started from the project root.
Configure your database and environment variables
Before you can actually start building, you need to create a Postgres database and configure your server to connect with it using environment variables:
Create a Postgres database with a name of your choice. This will just be the name of your local database so it doesn't need to match with the other members of your team.
In the
server/folder, make a new file called.envand copy the contents of.env.templateinto your.envfile.Update the values in
.envto match your Postgres database information (username, password, database name)Replace the
SESSION_SECRETvalue with your own random string. This is used to encrypt the cookie'suserIdvalue.Use a tool like https://randomkeygen.com/ to help generate the secret.
Your
.envfile should look something like this:
Remember, these values are used in the Knex configuration file server/knexfile.js. Pay attention to how the connection configuration is set, :
The connection configuration determines how Knex connects to your database.
In
developmentmode, assuming that thePG_CONNECTION_STRINGvalue is blank, then Knex will use the configuration object withhost,port,user,passwordanddatabaseset by your environment variables. If those values are also blank, there are default values provided.When you eventually deploy your database on Render, you will be given a
PG_CONNECTION_STRINGvalue from Render. You can add this to your environment variables to connect directly to your deployed database. If aPG_CONNECTION_STRINGvalue exists, Knex will use it to connect to your deployed database, not your local one.In
productionmode (which is used when you deploy your server application), a deployed database and thePG_CONNECTION_STRINGis expected.
The deployed database works exactly like your local database, so you are welcome to use a deployed database during testing as well. That said, the queries may take more time to execute since they are being transferred via the internet rather than between ports on your own machine.
Kickstart the project
With everything configured, you can now install dependencies in the frontend folder (React, etc...) and in the server folder (express, Knex, etc...) and run the provided migrations and seeds:
After installing dependencies and running the migrations and seeds, you should see that a users table has been created and seeded with three users. Check out the server/db/ folder to see how migrations and seeds were configured.
Finally, split the terminal (or open two terminals) and run npm run dev in both the frontend/ directory and in the server/ directory.
You're all set up now. Have fun!
With the application running, try exploring the user flow on the various pages as shown above.
Then, read the remaining sections to learn about the implementation details of the repository and how to work with it.
Recommended Approach for Building
When building a Full-stack application with so many components, you may struggle to identify the best place to start and how to test along the way.
This document is structured to reflect our recommended approach. We start with the database, then move to the server, then finish with the front-end. At a high level, you can think about working and testing your application from right to left in the diagram below:
The reason for this has to do with minimizing pre-requisites. The units further to the left in the diagram have depend on the functionality of the units to their right. For example, before you can build endpoints, you need controllers and in order to have controllers you need to have models. Before you can build models you need to create the tables in the database.
Testing Along the Way
After you complete one unit of the application make sure to test that unit before moving on. Here is how you can test each unit of the application:
Database Table / Migrations: Run the migration files and check your database using TablePlus, Postico, or the
psqlCLI.Model: Create a seed file that uses the model to insert hard-coded values into your table.
Controllers / Endpoints: Use Postman to send HTTP requests and check your server logs and your database to confirm requests received and the intended actions were taken.
Adapters: Run the adapter file with hard-coded calls to your adapter functions. Check your server logs and your database to confirm requests received and the intended actions were taken.
Components: Interact with the frontend application as if you were a real user. Check your server logs and your database to confirm requests received and the intended actions were taken.
Database
Chapters in this Section:
For this project, you should start with a Postgres database. Make sure to set the environment variables for connecting to this database in the .env file. These values are loaded into the knexfile.js file using the dotenv package and the line of code:
Migrations
For an overview of migrations and seeds, check out the chapter on Migrations and Seeds.
Migration files are stored in the server/db/migrations folder. To run them, run the following command:
Below, you can see the migration files that generate the users table. The first one sets up some initial columns:
This migration file will create a users table with an auto-generated and auto-incrementing id column, as well as username and password_hash columns.
Modifying / Adding New Migrations
As you build your project, you will likely want to modify your tables. If this is the case, AVOID using the migration:rollback unless you are willing to lose all data in your database and re-seed.
If you wish to keep existing data, you can create a new migration that modifies the table:
For example, the second migration file adds some timestamp columns to the existing users table.
Note that instead of using knex.schema.createTable, we are using .alterTable since the table already exists. We also use .alterTable in the .down function to drop the two columns created by table.timestamps if we ever did want to roll back these changes.
For more information, look into the Knex documentation
Seeds
There is a single seed file stored in the server/db/seeds folder. Instead of creating new seed files for your other tables, simply add additional seed data into this init.js file.
Run the seed file with:
The provided init.js seed file uses the User.create model method to generate the following data:

Notice how the passwords have been hashed! This is because the User.create method takes care of hashing passwords for us using bcrypt.
The Server Application
Chapters in this Section:
Server Overview
The server acts as the key middleman between the client / frontend application and the database. It is responsible for serving the React project's static assets as well as receiving and parsing client requests, getting data from the database, and sending responses back to the client.
To design a server that performs these interactions consistently and predictably, ask yourself:
What does the server expect from the frontend?
What does the frontend expect back from the server?
What does the database expect from the server?
What does the server expect back from the database?
The server is organized into a few key components (from right to left in the diagram):
The "Models" found in
server/models/They are responsible for interacting directly with and returning data from the database.
In this application, the models will use Knex to execute SQL queries.
The "Controllers" found in
server/controllers/They are responsible for parsing incoming requests, performing necessary server-side logic (like logging requests and interacting using models), and sending responses.
The "App" found in
server/index.jsThe hub of the server application, created by Express.
It is responsible for defining the endpoint URLs that will be available in the application and assigning controllers to handle each endpoint.
It also configures any middleware that will intercept requests.
User Model
As mentioned above, a model is the right-most component of a server application.
A model interacts directly with the database and can be used by controllers as an interface to the database.
An application can have many models and each model is responsible for managing interactions with a particular table in a database. In this template, a single model exists to manage interactions with the
userstable.
The User model (defined in server/db/models/User.js) provides static methods for performing CRUD operations with the users table in the database:
User.create(username, password)User.list()User.find(id)User.findByUsername(username)User.update(id, username)User.deleteAll()
In our tech stack, our model uses Knex to execute SQL queries that create, read, update, or delete data from the database. For example, User.find queries for a single user in the database with knex.raw()
Each User.X model method follows the same pattern:
Construct a query
Execute the query with
knex.raw, making sure to insert variablesReturn the data (for users, we wrap the data in a
new User. We'll look at why in the chapters below)
Each User method is used by one or more controllers when an API endpoint is requested by a client.
Controllers and API endpoints
A controller is a function that is invoked when a particular endpoint of the server API is requested. Controllers parse the request for data, invoke the appropriate methods of the models to interact with the database, and send responses back to the client.
The template provides API endpoints and controllers that are divided into two categories:
Authentication Endpoints: These endpoints enable the client to perform actions related to authenticating (registering a new account and logging in / out).
User Endpoints: These endpoints enable the client to perform various CRUD actions relating to User data.
Authentication Endpoints
POST
/api/auth/register
Create a new user and set the cookie userId
registerUser
create()
POST
/api/auth/login
Log in to an existing user and set cookie userId value
loginUser
findByUsername()
GET
/api/auth/me
Get the current logged in user based on the cookie
showMe
find()
DELETE
/api/auth/logout
Log the current user out (delete the cookie)
logoutUser
None
User Endpoints
GET
api/users
Get the list of all users
listUsers
list()
GET
/api/users/:id
Get a specific user by id
showUser
find()
PATCH
/api/users/:id
Update the username of a specific user by id
updateUser
update()
Creating New Users: Securing Passwords with Bcrypt
For more details on how
bcryptworks, read the chapter on Hashing Passwords with Bcrypt.
When starting this project, we seeded our database with users that had hashed passwords. This password hashing happens thanks to Bcrypt!
Open up the server/models/User file and at the top you will see bcrypt is imported. This module provides two functions: hash and compare. Here is how they are used:
When a user first creates an account, a controller invokes
User.create(). This method hashes the user's given password withbcrypt.hash(). Then, the hashed password will be stored in the database alongside the username.
Hashed passwords are stored in the database alongside usernames. When a user logs in, a controller invokes
User.findByUsername()to find the hashed password of the given user.bcrypt.compareis then used to compare this found password with the given password to see if they match.
The server uses the given username to find the associated hashed password in the database. If the given password produces the same hash, then the user is authenticated!
User.create() vs. the User constructor
User.create() vs. the User constructorWhen someone registers a new user, the User.create() method is used, not the constructor method. Let's see why.
To create a new user in the database, the User.create() static method can be invoked with a username and password. The method hashes the password before inserting it into the database.
knex.raw returns an object with a .rows property containing the requested data from the database. The .rows value is always an array, so we grab just the first value which will be the new user.
We will get something like this from the database:
Before returning, we make a User instance using the constructor function. The constructor takes in an object with the exact properties of the user table in the database and stores the password_hash as a private property:
Instances also have access to the isValidPassword instance method which can be used for re-authentication (see below).
As a result, the data that we end up sending to the client looks like this:
By using the constructor this way, the password is hidden before we send it to the client.
Take a look at each static method of the User class and you'll find that this pattern is repeated:
Data is retrieved from the database (including
password_hashvalues)Every user object is converted into a
Userinstance to keep thepassword_hashvalues safely contained within the model.The user objects can then be safely returned and used by the controllers.
Logging In: Authentication and Authorization
Authenticated means "We have confirmed this person is a real user and is allowed to be here"
For example, only logged-in users can see the other users in this app
Session authentication means that users who have recently provided their credentials do not need to log in again.
Authorized means "This person is allowed to perform this protected action"
For example, users are only authorized to edit their OWN profile (they can't change someone else's profile)
To implement this functionality, we'll use cookies.
Cookies and Session Authentication
In the context of computing and the internet, a cookie is a small text file sent by a server to a client and stored on the client along with where the cookie came from. Cookies are saved across browser sessions by default, meaning they will persist after the browser is closed. If a client has a cookie, it will automatically send it with future requests to the server the cookie came from.

To enable session authentication, when a user logs in, the server sends back a cookie with their user ID inside. That way, whenever the server receives a request with a cookie, it knows:
The user had previously signed in (otherwise, they wouldn't have a cookie)
Who sent the request based on the ID stored inside the cookie
If there is no cookie in the request (perhaps the user has cleared their cookies or is a new user), then the user must provide their credentials again to get a new cookie.
Cookies can also be used to control access to protected resources. Some resources require authentication only (the request must have a cookie):
For example, a user may need to be authenticated in order to access comments on a post.
Additionally, certain actions may be protected depending on the resource being requested and who made the request (the request needs a cookie AND that cookie needs a particular ID inside)
For example, if user
5sends a request to edit the profile of user8, that request will be rejected with a 403 unauthorized response. Only a request sent by user8is authorized to edit the profile of user8. In this case, the sender must have a cookie with the id8inside.
So, we want to send the client a cookie with their user ID stored inside. This means we need a way to:
Create a cookie
Put the user ID inside
Generating Cookies with Handle Cookie Sessions
To generate cookies, we'll use middleware provided by the cookie-session Node module. This middleware is configured in the handleCookieSessions file:
Here's what's happening:
The
name: 'session'configuration means that this middleware will put cookie data inside an objectreq.session. If we ever want to add data to a cookie, all we have to do is add data toreq.sessionwithin a controller.The
secret: process.env.SESSION_SECRETconfiguration sets a secret string used to encode all data stored in cookies so that the data can't easily be read.
If a user sends a request with no cookie, this middleware will create a new empty object stored at req.session and send it back with the response.
If a cookie DOES exist, this middleware will parse the cookie, and its data will be added to req.session.
Try modifying the logRoutes middleware to see incoming requests that have cookies:
Adding the User ID to a Cookie on Login
Remember, the cookie keeps our users logged in and authenticates and authorizes them on future requests. Therefore, we want to give cookies only to users who have logged in (or to users who have just registered).
To see this in action, take a look at the controller that handles login requests:
As you can see, after validating the user's credentials and right before sending the response, we add the user's ID to the cookie by setting req.session.userId:
Session Authentication and Authorization
We choose to store the user.id value in the cookie so that when it comes back with future requests, we can know who sent the request by looking at req.session.userId.
For example, when a user returns to the site, the client automatically sends a request to the GET /api/auth/me endpoint, which uses this auth controller:
Without needing to log in again, the /api/auth/me endpoint checks to see if a cookie exists, and if it does, fetches the appropriate user!
You'll also notice this req.session value is checked in the checkAuthentication middleware which requires a cookie for certain endpoints to be used:
Lastly, req.session is also checked to authorize a user to update their profile in the PATCH /api/users/:id controller:
To paint the picture clearly, this is how the cookie is passed back and forth between client and server enabling authorization:
Front-end
Chapters in this Section
The front-end is responsible for handling user interactions, sending requests to the server application, and rendering content provided by the server.
While it is developed as a React application and .jsx files, it will ultimately be built into static assets (HTML, CSS, and JS files that can be sent directly to the browser).
The frontend application is organized into a few key components (from right to left in the diagram):
The "Adapters" found in
frontend/src/adapters/Responsible for structuring requests sent to the server and for parsing responses.
The front-end equivalent of controllers
The "Pages" found in
frontend/src/pages/Responsible for rendering separate pages of the front-end application.
These components make use of sub-components defined in
frontend/src/components
The "App" found in
frontend/src/App.jsxThe root component that is responsible for defining frontend routes and establishing site-wide layout components (like the navigation bar)
The
frontend/main.jsxfileRenders the
AppcomponentProvides access to the
BrowserRouterand the application's global Context.
The
index.htmlfileLoads the
main.jsxfile and any additional scripts.
Frontend Utils
Let's again start at the right end of the diagram and talk about fetching. Provided in the frontend/src/utils/fetchingUtils.js file are a series of helper functions for formatting a fetch request.
The fetchHandler function will actually send the fetch request, making sure that the response is valid and that the response is in JSON format before parsing.
If the front-end wants to make a POST/PATCH/DELETE request, an options object must be provided. For example, this options object for a POST:
Since these objects are mostly boilerplate, there are helpers for creating those options objects. For example, the getPostOptions function can be used like this
Adapters
An adapter is another layer of abstraction around the fetching process. Really, they are just helper functions for fetching from a specific server endpoint.
Often, they will be short, like this from the adapters/user-adapter.js file:
A
baseUrlis defined for all adapters in thisuser-adapterfile to simplify building URLsThe
fetchHandlerwill return a[data, error]tuple which we can return, passing both values along to the component that uses it. We let the component handle the error.
This separation of concerns keeps our component files a bit cleaner while also allowing multiple components to fetch from the same endpoint if needed.
Errors are handled in the components that use these adapters.
Example Page Component
Let's look at that Users page component! It is a great example of a page component that uses an adapter to fetch data from the server and gracefully handle the response data and error.
Let's break down the component:
State is created for the
usersanderrorthat we expect to get in return from the server when we fetch for users.On render, the component uses the
getAllUsersadapter to fetch the data and set either theusersstate or theerrorstate, depending on what is returned.The component renders an error message if the
errorstate is set.Otherwise, the
usersstate is mapped into a list of elements and rendered.
Current User Context
This application uses React Context to share the current logged-in user throughout the entire application. Many pages will need to know if a user is logged in
The frontend uses a CurrentUserContext to provide the entire application with the currently logged in user and a function to set the currently logged in user.
The first component to use this context is App which sets the current user after a successful GET /api/me request (the user had a cookie indicating they previously signed in). This is the first thing that happens whenever a user visits the web application.
Once the currentUser is set in context, it can be used by any page.
For example, the pages/Login page redirects users away from the page if the currentUser value is set (we don't want signed-in users to be able to view the login page). It uses the currentUser.id value to redirect the user to their specific profile page.
Below are the pages/components that use the context:
components/SiteHeadingAndNavif a user is logged in show a link to view their own profile and a link to see all users, otherwise show the login/sign up buttons in the nav
pages/Loginif a user is already logged in, it navigates back to the home page.
otherwise, this page can set the current user after a successful
POST /api/loginrequest
pages/SignUpif a user is already logged in, it navigates back to the home page.
otherwise, this page can set the current user after a successful
POST /api/usersrequest
pages/Userif the currently logged in user matches the current profile page, the user can edit the profile and log out
if the user logs out, it sets the current logged in user to
nullbefore navigating back home.
Deploying
For instructions on deployment, check out the Marcy Lab School Docs guide on How to Deploy On Render making sure to follow the instructions for deploying both a server and a database.
Last updated