14. React Express Auth Template Overview
Last updated
Last updated
Table of Contents
This template repository provides functional, but basic, user management. Users can:
Register a new account with a username and password
Log in to their account
View a list of all users
View a single user's page
Update their username
Log out
Below, you can see the user journey:
When a user visits the "/"
page, they see the Home page and options to either login or signup.
Returning users will be automatically signed in via the GET /api/auth/me
endpoint.
Users can authenticate by visiting the "/login"
or "/sign-up"
pages.
Login requests are sent to the POST /api/auth/login
endpoint.
Sign up requests are sent to the POST /api/auth/register
endpoint.
After logging in or signing up users are taken to the "/users/:id"
page for 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 fetched via the GET /api/users/:id
endpoint.
Username updates are fetched via the PATCH /api/users/:id
endpoint which requires authorization.
User log out is fetched via the DELETE /api/auth/logout
endpoint.
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.
All users data is fetched via the GET /api/users
endpoint.
The ERD for this application is simple. Just a single users
table:
Now that you understand what this app can do, let's jump in and get it running locally!
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.
In the root of this repository are the two parts 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 project 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.
Before you can actually start building, you need to create a database and configure your server to connect with it.
Create a 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, copy the .env.template
and name it .env
.
Update the .env
variables to match your Postgres database information (username, password, database name)
Replace the SESSION_SECRET
value with your own random string. This is used to encrypt the cookie's userId
value.
Your .env
file 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 development
mode, if the PG_CONNECTION_STRING
is blank, then Knex will use the configuration object with host
, port
, user
, password
and database
.
However, when you eventually deploy your database on Render, you will be given a PG_CONNECTION_STRING
value from Render. You can add this to your environment variables to connect directly to your deployed database. If a PG_CONNECTION_STRING
value exists, Knex will use it to connect to your deployed database.
In production
mode, the PG_CONNECTION_STRING
is 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.
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:
In the future, you can also run the
npm run kickstart
command which will do all of this for you!
As a result of 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 and cd
into the frontend/
application and server/
application. Then start each application using npm run dev
in each directory.
Below, you will find more information about this repository and how to work with it. Enjoy!
Chapters in this Section:
For this project, you should use 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:
Migration files are stored in the server/db/migrations
folder. Here, 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.
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.
Seed files are stored in the server/db/seeds
folder.
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
.
Chapters in this Section:
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/
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/
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.js
The hub of the server application, created by Express.
Responsible for defining the endpoint URLs that will be available in the application and assigning controllers to handle each endpoint.
It also configures middleware.
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 our tech stack, our models use Knex to execute SQL statements.
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()
Each method invokes knex.raw()
and executes a SQL query to create, read, update, or delete data from the database. For example, User.find
queries for a single user in the database:
Each method follows the same pattern:
Construct a query
Execute the query with knex.raw
, making sure to insert variables
Return 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.
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()
Remember how the users that we seeded into our database had their passwords hashed? This 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, the controller invokes the User.create()
method which hashes the user's given password with bcrypt.hash()
. Then, the hashed password will be stored in the database alongside the username.
When a user logs in, a controller uses the User.findByUsername()
method to find the hashed password, and then we can use bcrypt.compare
to see if the given password generates the same hash as the one stored in the database.
User.create()
vs. the User
constructorDid you notice that there is both a User.create()
method AND a constructor()
? 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:
As a result, 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_hash
values)
Every user object is converted into a User
instance to keep the password_hash
values safely contained within the model.
The user objects can then be safely returned and used by the controllers.
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.
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
5
sends a request to edit the profile of user8
, that request will be rejected with a 403 unauthorized response. Only a request sent by user8
is authorized to edit the profile of user8
. In this case, the sender must have a cookie with the id8
inside.
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
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 object req.session
. If we ever want to add data to a cookie, all we have to do is add data to req.session
within a controller.
The secret: process.env.SESSION_SECRET
configuration 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:
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
:
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:
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.jsx
The root component that is responsible for defining frontend routes and establishing site-wide layout components (like the navigation bar)
The frontend/main.jsx
file
Renders the App
component
Provides access to the BrowserRouter
and the application's global Context.
The index.html
file
Loads the main.jsx
file and any additional scripts.
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
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 baseUrl
is defined for all adapters in this user-adapter
file to simplify building URLs
The fetchHandler
will 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.
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 users
and error
that we expect to get in return from the server when we fetch for users.
On render, the component uses the getAllUsers
adapter to fetch the data and set either the users
state or the error
state, depending on what is returned.
The component renders an error message if the error
state is set.
Otherwise, the users
state is mapped into a list of elements and rendered.
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/SiteHeadingAndNav
if 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/Login
if 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/login
request
pages/SignUp
if 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/users
request
pages/User
if 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 null
before navigating back home.
Use a tool like to help generate the secret.
For an overview of migrations and seeds, check out the chapter on .
For more information, look into the
For more details on how bcrypt
works, read the chapter on .
This application uses to share the current logged-in user throughout the entire application. Many pages will need to know if a user is logged in
For instructions on deployment, check out the Marcy Lab School Docs guide on making sure to follow the instructions for deploying both a server and a database.